File

src/registrar/registrar.service.ts

Index

Properties
Methods

Constructor

constructor(configService: ConfigService, cryptoService: CryptoService, presentationsService: PresentationsService)
Parameters :
Name Type Optional
configService ConfigService No
cryptoService CryptoService No
presentationsService PresentationsService No

Methods

Private Async addAccessCertificate
addAccessCertificate(config: RegistrarConfig, tenantId: string)

Add a new access certificate to the registrar. This is only needed once, when the access certificate is created. If the access certificate already exists, it will be returned.

Parameters :
Name Type Optional
config RegistrarConfig No
tenantId string No
Returns : Promise<string>
Async addRegistrationCertificate
addRegistrationCertificate(req: RegistrationCertificateRequest, dcql_query: any, requestId: string, tenantId: string)

Add a new registration certificate to the registrar. This is only needed once, when the registration certificate is created. If the registration certificate already exists, it will be returned.

Parameters :
Name Type Optional
req RegistrationCertificateRequest No
dcql_query any No
requestId string No
tenantId string No
Returns : unknown
addRp
addRp(tenantId: string)

Add a new relying party to the registrar. This is only needed once, when the relying party is created.

Parameters :
Name Type Optional
tenantId string No
Returns : Promise<string>
Async getAccessCertificateId
getAccessCertificateId(config: RegistrarConfig, tenantId: string)

Get the access certificate ID from the registrar. If there is no access certificate ID in the config, it will add a new one. If there is one, it will check if it is still valid. If it is revoked, it will add a new one.

Parameters :
Name Type Optional
config RegistrarConfig No
tenantId string No
Returns : any
isEnabled
isEnabled()
Returns : boolean
Private loadConfig
loadConfig(tenantId: string)

Load the registrar configuration from the config file.

Parameters :
Name Type Optional
tenantId string No
Returns : RegistrarConfig
Async onApplicationBootstrap
onApplicationBootstrap()

This function is called when the module is initialized. It will refresh the access token and add the relying party and certificates to the registrar.

Returns : any
onModuleInit
onModuleInit()
Returns : void
Async onTenantInit
onTenantInit(tenantId: string)
Decorators :
@OnEvent(TENANT_EVENTS.TENANT_KEYS, {async: true})

This function is called when a tenant is initialized.

Parameters :
Name Type Optional
tenantId string No
Returns : any
Async refreshAccessToken
refreshAccessToken()

Get the access token from OIDC provider using client credentials grant.

Returns : any
Private saveConfig
saveConfig(config: RegistrarConfig, tenantId: string)

Save the registrar configuration to the config file.

Parameters :
Name Type Optional
config RegistrarConfig No
tenantId string No
Returns : void
Private storeExistingRp
storeExistingRp(name: string)
Parameters :
Name Type Optional
name string No
Returns : any

Properties

Private accessToken
Type : string
Private client
Private oauth2Client
Type : OAuth2Client
import {
    Injectable,
    OnApplicationBootstrap,
    OnModuleInit,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { OAuth2Client } from '@badgateway/oauth2-client';
import { client } from './generated/client.gen';
import {
    accessCertificateControllerFindOne,
    accessCertificateControllerRegister,
    registrationCertificateControllerAll,
    registrationCertificateControllerRegister,
    relyingPartyControllerFindAll,
    relyingPartyControllerRegister,
} from './generated';
import { CryptoService } from '../crypto/crypto.service';
import { RegistrationCertificateRequest } from '../verifier/presentations/dto/vp-request.dto';
import { PresentationsService } from '../verifier/presentations/presentations.service';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { RegistrarConfig } from './registrar-config';
import { join } from 'node:path';
import { OnEvent } from '@nestjs/event-emitter';
import { TENANT_EVENTS } from '../auth/tenant-events';

interface AccessCertificateResponse {
    id: string;
    crt: string;
    revoked?: boolean;
}

@Injectable()
export class RegistrarService implements OnApplicationBootstrap, OnModuleInit {
    private oauth2Client: OAuth2Client;
    private client: typeof client;
    private accessToken: string;

    constructor(
        private configService: ConfigService,
        private cryptoService: CryptoService,
        private presentationsService: PresentationsService,
    ) {}

    onModuleInit() {
        //when not set, we will not use the registrar
        if (!this.isEnabled()) {
            return;
        }

        const oidcIssuerUrl =
            this.configService.getOrThrow<string>('OIDC_ISSUER_URL');
        const clientId =
            this.configService.getOrThrow<string>('OIDC_CLIENT_ID');
        const clientSecret =
            this.configService.getOrThrow<string>('OIDC_CLIENT_SECRET');

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

        this.client = client;
        this.client.setConfig({
            baseUrl: this.configService.getOrThrow<string>('REGISTRAR_URL'),
            auth: () => this.accessToken,
        });
    }

    isEnabled() {
        return !!this.configService.get<string>('REGISTRAR_URL');
    }

    /**
     * This function is called when the module is initialized.
     * It will refresh the access token and add the relying party and certificates to the registrar.
     */
    async onApplicationBootstrap() {
        if (!this.configService.get<string>('REGISTRAR_URL')) {
            return;
        }
        await this.refreshAccessToken();
    }

    /**
     * This function is called when a tenant is initialized.
     * @param tenantId
     */
    @OnEvent(TENANT_EVENTS.TENANT_KEYS, { async: true })
    async onTenantInit(tenantId: string) {
        if (!this.isEnabled()) {
            return;
        }
        const config = this.loadConfig(tenantId);
        if (!config.id) {
            config.id = await this.addRp(tenantId);
        }
        await this.getAccessCertificateId(config, tenantId);
    }

    /**
     * Get the access token from OIDC provider using client credentials grant.
     */
    async refreshAccessToken() {
        await this.oauth2Client.clientCredentials().then((token) => {
            this.accessToken = token.accessToken;
            const date = new Date();
            const expirationDate = new Date(token.expiresAt as number);
            setTimeout(
                // eslint-disable-next-line @typescript-eslint/no-misused-promises
                () => this.refreshAccessToken(),
                expirationDate.getTime() - date.getTime() - 1000,
            );
        });
    }

    /**
     * Add a new relying party to the registrar.
     * This is only needed once, when the relying party is created.
     */
    addRp(tenantId: string): Promise<string> {
        const name = this.configService.getOrThrow<string>('RP_NAME');
        return relyingPartyControllerRegister({
            client: this.client,
            body: {
                name,
            },
        }).then(async (response) => {
            const config = this.loadConfig(tenantId);
            if (response.error) {
                config.id = await this.storeExistingRp(name);
            } else {
                config.id = response.data!['id'];
            }
            this.saveConfig(config, tenantId);
            return response.data!['id'];
        });
    }

    private storeExistingRp(name: string) {
        return relyingPartyControllerFindAll({
            client: this.client,
            query: {
                name,
            },
        }).then((response) => {
            return response.data!.find((item) => item.name === name)?.id;
        });
    }

    /**
     * Get the access certificate ID from the registrar.
     * If there is no access certificate ID in the config, it will add a new one.
     * If there is one, it will check if it is still valid.
     * If it is revoked, it will add a new one.
     * @param config
     */
    async getAccessCertificateId(config: RegistrarConfig, tenantId: string) {
        // if there is no access certificate ID in the config, we need to add it
        if (!config.accessCertificateId) {
            await this.addAccessCertificate(config, tenantId);
        }
        // if there is one, check if it is still valid
        await accessCertificateControllerFindOne({
            client: this.client,
            path: { rp: config.id, id: config.accessCertificateId! },
        }).then((res) => {
            if (res.error) {
                console.error('Error finding access certificate:', res.error);
            }
            const data = res.data as AccessCertificateResponse;
            if (data.revoked) {
                console.warn('Access certificate is revoked, adding a new one');
                return this.addAccessCertificate(config, tenantId);
            }
        });
    }

    /**
     * Add a new access certificate to the registrar.
     * This is only needed once, when the access certificate is created.
     * If the access certificate already exists, it will be returned.
     * @returns
     */
    private async addAccessCertificate(
        config: RegistrarConfig,
        tenantId: string,
    ): Promise<string> {
        const host = this.configService
            .getOrThrow<string>('PUBLIC_URL')
            .replace('https://', '');
        return accessCertificateControllerRegister({
            client: this.client,
            body: {
                publicKey: await this.cryptoService.keyService.getPublicKey(
                    'pem',
                    tenantId,
                ),
                dns: [host],
            },
            path: {
                rp: config.id,
            },
        }).then((res) => {
            if (res.error) {
                console.error('Error adding access certificate:', res.error);
                throw new Error('Error adding access certificate');
            }
            //store the cert
            this.cryptoService.storeAccessCertificate(
                res.data!['crt'],
                tenantId,
            );
            config.accessCertificateId = res.data!['id'];
            this.saveConfig(config, tenantId);
            return res.data!['id'];
        });
    }

    /**
     * Add a new registration certificate to the registrar.
     * This is only needed once, when the registration certificate is created.
     * If the registration certificate already exists, it will be returned.
     * @returns
     */
    async addRegistrationCertificate(
        req: RegistrationCertificateRequest,
        //TODO: check if the dcql_query is covered by the registration certificate. If not, we need to throw an error since we do not know the new purpose for it.
        dcql_query: any,
        requestId: string,
        tenantId: string,
    ) {
        const rp = this.loadConfig(tenantId).id;

        //TODO: need to check if the access certificate is bound to the access certificate with the subject. Also that the requested fields are matching.

        const certs =
            (await registrationCertificateControllerAll({
                client: this.client,
                path: {
                    rp,
                },
            }).then((res) =>
                res.data?.filter(
                    (cert) =>
                        cert.revoked == null && cert.id === (req.id as string),
                ),
            )) || [];

        if (certs?.length > 0) {
            return certs[0].jwt;
        }

        return registrationCertificateControllerRegister({
            client: this.client,
            path: {
                rp,
            },
            body: req.body,
        }).then(async (res) => {
            if (res.error) {
                console.error(
                    'Error adding registration certificate:',
                    res.error,
                );
                throw new Error('Error adding registration certificate');
            }

            //TODO: write the ID to the config so its easier to use it. Easier than writing the comparison algorithm (any maybe someone wants to use a different one)
            await this.presentationsService.storeRCID(
                res.data!['id'],
                requestId,
                tenantId,
            );
            return res.data!['jwt'];
        });
    }

    /**
     * Load the registrar configuration from the config file.
     * @returns
     */
    private loadConfig(tenantId: string): RegistrarConfig {
        const filePath = join(
            this.configService.getOrThrow<string>('FOLDER'),
            tenantId,
            'registrar.json',
        );

        if (!existsSync(filePath)) {
            // If the config file does not exist, create an empty config
            const initialConfig: RegistrarConfig = {};
            writeFileSync(filePath, JSON.stringify(initialConfig, null, 2));
            return initialConfig;
        }
        const config = JSON.parse(
            readFileSync(filePath, 'utf-8'),
        ) as RegistrarConfig;
        return config;
    }

    /**
     * Save the registrar configuration to the config file.
     * @param config
     */
    private saveConfig(config: RegistrarConfig, tenantId: string) {
        const filePath = join(
            this.configService.getOrThrow<string>('FOLDER'),
            tenantId,
            'registrar.json',
        );
        writeFileSync(filePath, JSON.stringify(config, null, 2));
    }
}

results matching ""

    No results matching ""