File

src/registrar/registrar-auth.service.ts

Description

Handles OAuth2 token acquisition/caching and low-level registrar client creation. Also provides getRelyingPartyId which is shared by both access-certificate and registration-certificate flows.

Index

Properties
Methods

Constructor

constructor(configRepository: Repository<RegistrarConfigEntity>)
Parameters :
Name Type Optional
configRepository Repository<RegistrarConfigEntity> No

Methods

Async getAccessToken
getAccessToken(tenantId: string)

Get or refresh the access token for a tenant using the ROPC flow.

Parameters :
Name Type Optional
tenantId string No
Returns : Promise<string>
Async getClient
getClient(tenantId: string)

Create a configured registrar API client for the given tenant.

Parameters :
Name Type Optional
tenantId string No
Returns : unknown
Async getRelyingPartyId
getRelyingPartyId(tenantId: string)

Get the relying party ID from the registrar. If none exists yet, one is registered on the fly.

Parameters :
Name Type Optional
tenantId string No
Returns : Promise<string>
invalidateToken
invalidateToken(tenantId: string)

Remove the cached token for a tenant (e.g. after config changes).

Parameters :
Name Type Optional
tenantId string No
Returns : void
Async testCredentials
testCredentials(config: literal type)

Test OIDC credentials by attempting to obtain an access token.

Parameters :
Name Type Optional
config literal type No
Returns : Promise<void>

Properties

Private Readonly logger
Type : unknown
Default value : new Logger(RegistrarAuthService.name)
Private Readonly tokenCache
Type : unknown
Default value : new Map<string, CachedToken>()
import { OAuth2Client, OAuth2Token } from "@badgateway/oauth2-client";
import {
    BadRequestException,
    Injectable,
    Logger,
    NotFoundException,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { RegistrarConfigEntity } from "./entities/registrar-config.entity";
import {
    relyingPartyControllerFindAll,
    relyingPartyControllerRegister,
} from "./generated";
import { client as registrarClient } from "./generated/client.gen";

/**
 * Cached OAuth2 token with its expiration time.
 */
interface CachedToken {
    token: string;
    expiresAt: number;
}

/**
 * Handles OAuth2 token acquisition/caching and low-level registrar client
 * creation. Also provides {@link getRelyingPartyId} which is shared by both
 * access-certificate and registration-certificate flows.
 */
@Injectable()
export class RegistrarAuthService {
    private readonly logger = new Logger(RegistrarAuthService.name);

    private readonly tokenCache = new Map<string, CachedToken>();

    constructor(
        @InjectRepository(RegistrarConfigEntity)
        private readonly configRepository: Repository<RegistrarConfigEntity>,
    ) {}

    /**
     * Test OIDC credentials by attempting to obtain an access token.
     * @throws BadRequestException if authentication fails
     */
    async testCredentials(config: {
        oidcUrl: string;
        clientId: string;
        clientSecret?: string;
        username: string;
        password: string;
    }): Promise<void> {
        const oauth2Client = new OAuth2Client({
            server: `${config.oidcUrl}/protocol/openid-connect/token`,
            clientId: config.clientId,
            clientSecret: config.clientSecret,
            discoveryEndpoint: `${config.oidcUrl}/.well-known/openid-configuration`,
        });

        try {
            await oauth2Client.password({
                username: config.username,
                password: config.password,
            });
            this.logger.log("Registrar credentials validated successfully");
        } catch (error: any) {
            this.logger.warn(`Credential validation failed: ${error.message}`);
            throw new BadRequestException(
                `Failed to authenticate with registrar. Please check your credentials. Error: ${error.message}`,
            );
        }
    }

    /**
     * Get or refresh the access token for a tenant using the ROPC flow.
     */
    async getAccessToken(tenantId: string): Promise<string> {
        const cached = this.tokenCache.get(tenantId);
        if (cached && cached.expiresAt > Date.now() + 5000) {
            return cached.token;
        }

        const config = await this.configRepository.findOneBy({ tenantId });
        if (!config) {
            throw new NotFoundException(
                `No registrar configuration found for tenant ${tenantId}`,
            );
        }

        const oauth2Client = new OAuth2Client({
            server: `${config.oidcUrl}/protocol/openid-connect/token`,
            clientId: config.clientId,
            clientSecret: config.clientSecret,
            discoveryEndpoint: `${config.oidcUrl}/.well-known/openid-configuration`,
        });

        let tokenResponse: OAuth2Token;
        try {
            tokenResponse = await oauth2Client.password({
                username: config.username,
                password: config.password,
            });
        } catch (error: any) {
            this.logger.error(
                `[${tenantId}] Failed to obtain access token: ${error.message}`,
            );
            throw new BadRequestException(
                `Failed to authenticate with registrar: ${error.message}`,
            );
        }

        const expiresAt =
            typeof tokenResponse.expiresAt === "number"
                ? tokenResponse.expiresAt
                : Date.now() + 3600 * 1000;

        this.tokenCache.set(tenantId, {
            token: tokenResponse.accessToken,
            expiresAt,
        });

        return tokenResponse.accessToken;
    }

    /**
     * Create a configured registrar API client for the given tenant.
     */
    async getClient(tenantId: string) {
        const config = await this.configRepository.findOneBy({ tenantId });
        if (!config) {
            throw new NotFoundException(
                `No registrar configuration found for tenant ${tenantId}`,
            );
        }

        const client = registrarClient;
        const accessToken = await this.getAccessToken(tenantId);

        client.setConfig({
            baseUrl: config.registrarUrl,
            auth: () => accessToken,
        });

        return client;
    }

    /**
     * Get the relying party ID from the registrar.
     * If none exists yet, one is registered on the fly.
     */
    async getRelyingPartyId(tenantId: string): Promise<string> {
        const client = await this.getClient(tenantId);

        const res = await relyingPartyControllerFindAll({ client });
        if (res.error) {
            this.logger.error(
                { error: res.error },
                `[${tenantId}] Failed to fetch relying parties`,
            );
            throw new BadRequestException(
                "Failed to fetch relying parties from registrar",
            );
        }

        const relyingParties = res.data || [];
        if (relyingParties.length === 0) {
            const createRes = await relyingPartyControllerRegister({
                client,
                body: {},
            });
            return createRes.data!.id;
        }

        return relyingParties[0].id;
    }

    /**
     * Remove the cached token for a tenant (e.g. after config changes).
     */
    invalidateToken(tenantId: string): void {
        this.tokenCache.delete(tenantId);
    }
}

results matching ""

    No results matching ""