File

src/verifier/oid4vp/oid4vp.service.ts

Index

Properties

Properties

session
session: string
Type : string
Optional
webhook
webhook: WebhookConfig
Type : WebhookConfig
Optional
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { randomUUID } from 'node:crypto';
import { CryptoService } from '../../crypto/crypto.service';
import { AuthorizationResponse } from './dto/authorization-response.dto';
import { RegistrarService } from '../../registrar/registrar.service';
import {
    AuthResponse,
    PresentationsService,
} from '../presentations/presentations.service';
import { EncryptionService } from '../../crypto/encryption/encryption.service';
import { v4 } from 'uuid';
import { SessionService } from '../../session/session.service';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import { OfferResponse } from '../../issuer/oid4vci/dto/offer-request.dto';
import { WebhookConfig } from '../../utils/webhook.dto';
import {
    SessionLoggerService,
    SessionLogContext,
} from '../../utils/session-logger.service';

export interface PresentationRequestOptions {
    session?: string;
    webhook?: WebhookConfig;
}

@Injectable()
export class Oid4vpService {
    constructor(
        private cryptoService: CryptoService,
        private encryptionService: EncryptionService,
        private configService: ConfigService,
        private registrarService: RegistrarService,
        private presentationsService: PresentationsService,
        private sessionService: SessionService,
        private httpService: HttpService,
        private sessionLogger: SessionLoggerService,
    ) {}

    /**
     * Creates an authorization request for the OID4VP flow.
     * This method generates a JWT that includes the necessary parameters for the authorization request.
     * It initializes the session logging context and logs the start of the flow.
     * @param requestId
     * @param tenantId
     * @param auth_session
     * @returns
     */
    async createAuthorizationRequest(
        requestId: string,
        tenantId: string,
        auth_session: string,
    ): Promise<string> {
        // Create session logging context
        const logContext: SessionLogContext = {
            sessionId: auth_session,
            tenantId,
            flowType: 'OID4VP',
            stage: 'authorization_request',
        };

        this.sessionLogger.logFlowStart(logContext, {
            requestId,
            action: 'create_authorization_request',
        });

        try {
            const host = this.configService.getOrThrow<string>('PUBLIC_URL');
            const tenantUrl = `${host}/${tenantId}`;

            const values =
                await this.presentationsService.getPresentationConfig(
                    requestId,
                    tenantId,
                );
            let regCert: string | undefined = undefined;

            const dcql_query = JSON.parse(
                JSON.stringify(values.dcql_query).replace(
                    /<PUBLIC_URL>/g,
                    tenantUrl,
                ),
            );

            if (this.registrarService.isEnabled()) {
                const registrationCert = JSON.parse(
                    JSON.stringify(values.registrationCert).replace(
                        /<PUBLIC_URL>/g,
                        tenantUrl,
                    ),
                );
                regCert =
                    await this.registrarService.addRegistrationCertificate(
                        registrationCert,
                        dcql_query,
                        requestId,
                        tenantId,
                    );
            }
            const nonce = randomUUID();
            await this.sessionService.add(auth_session, tenantId, {
                vp_nonce: nonce,
            });

            this.sessionLogger.logAuthorizationRequest(logContext, {
                requestId,
                nonce,
                regCert,
                dcqlQueryCount: Array.isArray(dcql_query)
                    ? dcql_query.length
                    : 1,
            });

            const request = {
                payload: {
                    response_type: 'vp_token',
                    client_id: 'x509_san_dns:' + host.replace('https://', ''),
                    response_uri: `${host}/${tenantId}/oid4vp/response`,
                    response_mode: 'direct_post.jwt',
                    nonce,
                    dcql_query,
                    client_metadata: {
                        jwks: {
                            keys: [
                                this.encryptionService.getEncryptionPublicKey(),
                            ],
                        },
                        vp_formats: {
                            mso_mdoc: {
                                alg: ['EdDSA', 'ES256', 'ES384'],
                            },
                            'dc+sd-jwt': {
                                'kb-jwt_alg_values': [
                                    'EdDSA',
                                    'ES256',
                                    'ES384',
                                    'ES256K',
                                ],
                                'sd-jwt_alg_values': [
                                    'EdDSA',
                                    'ES256',
                                    'ES384',
                                    'ES256K',
                                ],
                            },
                        },
                        authorization_encrypted_response_alg: 'ECDH-ES',
                        authorization_encrypted_response_enc: 'A128GCM',
                        client_name:
                            this.configService.getOrThrow<string>('RP_NAME'),
                        response_types_supported: ['vp_token'],
                    },
                    state: auth_session,
                    aud: host,
                    exp: Math.floor(Date.now() / 1000) + 60 * 5,
                    iat: Math.floor(new Date().getTime() / 1000),
                    verifier_attestations: regCert
                        ? [
                              {
                                  format: 'jwt',
                                  data: regCert,
                              },
                          ]
                        : undefined,
                },
                header: {
                    typ: 'oauth-authz-req+jwt',
                },
            };

            let accessCert: string[] | undefined = undefined;
            try {
                accessCert = this.cryptoService.getCertChain(
                    'access',
                    tenantId,
                );
                // eslint-disable-next-line @typescript-eslint/no-unused-vars
            } catch (err: any) {
                accessCert = this.cryptoService.getCertChain(
                    'signing',
                    tenantId,
                );
            }

            const header = {
                ...request.header,
                alg: 'ES256',
                x5c: accessCert,
            };

            const signedJwt = await this.cryptoService.signJwt(
                header,
                request.payload,
                tenantId,
            );

            this.sessionLogger.logSession(
                logContext,
                'Authorization request created successfully',
                {
                    signedJwtLength: signedJwt.length,
                    certificateChainLength: accessCert?.length || 0,
                },
            );

            return signedJwt;
        } catch (error) {
            this.sessionLogger.logFlowError(logContext, error as Error, {
                requestId,
                action: 'create_authorization_request',
            });
            throw error;
        }
    }

    /**
     * Creates a request for the OID4VP flow.
     * @param requestId
     * @param values
     * @param tenantId
     * @returns
     */
    async createRequest(
        requestId: string,
        values: PresentationRequestOptions,
        tenantId: string,
    ): Promise<OfferResponse> {
        const presentationConfig =
            await this.presentationsService.getPresentationConfig(
                requestId,
                tenantId,
            );

        if (!values.session) {
            values.session = v4();
            await this.sessionService.create({
                id: values.session,
                webhook: values.webhook ?? presentationConfig.webhook,
                tenantId,
            });
        } else {
            await this.sessionService.add(values.session, tenantId, {
                webhook: values.webhook ?? presentationConfig.webhook,
            });
        }

        const host = this.configService
            .getOrThrow<string>('PUBLIC_URL')
            .replace('https://', '');
        const params = {
            client_id: `x509_san_dns:${host}`,
            request_uri: `${this.configService.getOrThrow<string>('PUBLIC_URL')}/${tenantId}/oid4vp/request/${requestId}/${values.session}`,
        };
        const queryString = Object.entries(params)
            .map(
                ([key, value]) =>
                    `${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
            )
            .join('&');

        return {
            uri: queryString,
            session: values.session,
        };
    }

    /**
     * Processes the response from the wallet.
     * @param body
     * @param tenantId
     */
    async getResponse(body: AuthorizationResponse, tenantId: string) {
        const res = await this.encryptionService.decryptJwe<AuthResponse>(
            body.response,
        );
        const session = await this.sessionService.get(res.state);

        // Create session logging context
        const logContext: SessionLogContext = {
            sessionId: res.state,
            tenantId,
            flowType: 'OID4VP',
            stage: 'response_processing',
        };

        this.sessionLogger.logFlowStart(logContext, {
            action: 'process_presentation_response',
            hasWebhook: !!session.webhook,
        });

        try {
            //TODO: load required fields from the config
            const credentials = await this.presentationsService.parseResponse(
                res,
                [],
                session.vp_nonce as string,
            );

            this.sessionLogger.logCredentialVerification(
                logContext,
                !!credentials && credentials.length > 0,
                {
                    credentialCount: credentials?.length || 0,
                    nonce: session.vp_nonce,
                },
            );

            //tell the auth server the result of the session.
            await this.sessionService.add(res.state, tenantId, {
                //TODO: not clear why it has to be any
                credentials: credentials as any,
            });
            // if there a a webook URL, send the response there
            if (session.webhook) {
                const headers: Record<string, string> = {};
                if (
                    session.webhook.auth &&
                    session.webhook.auth.type === 'apiKey'
                ) {
                    headers[session.webhook.auth.config.headerName] =
                        session.webhook.auth.config.value;
                }

                console.log(headers);

                this.sessionLogger.logSession(
                    logContext,
                    'Sending webhook notification',
                    {
                        webhookUrl: session.webhook.url,
                        authType: session.webhook.auth?.type || 'none',
                    },
                );

                await firstValueFrom(
                    this.httpService.post(
                        session.webhook.url,
                        {
                            credentials,
                            session: res.state,
                        },
                        {
                            headers,
                        },
                    ),
                ).then(
                    async (webhookResponse) => {
                        //TODO: better: just store it when it's a presentation during issuance
                        if (webhookResponse.data) {
                            session.credentialPayload!.values =
                                webhookResponse.data;
                            //store received webhook response
                            await this.sessionService.add(res.state, tenantId, {
                                credentialPayload: session.credentialPayload,
                            });
                        }

                        this.sessionLogger.logSession(
                            logContext,
                            'Webhook notification sent successfully',
                            {
                                responseStatus: webhookResponse.status,
                                hasResponseData: !!webhookResponse.data,
                            },
                        );
                    },
                    (err) => {
                        this.sessionLogger.logSessionError(
                            logContext,
                            err,
                            'Error sending webhook',
                            {
                                webhookUrl: session.webhook!.url,
                            },
                        );
                        throw new Error(
                            `Error sending webhook: ${err.message || err}`,
                        );
                    },
                );
            }

            this.sessionLogger.logFlowComplete(logContext, {
                credentialCount: credentials?.length || 0,
                webhookSent: !!session.webhook,
            });
        } catch (error) {
            this.sessionLogger.logFlowError(logContext, error as Error, {
                action: 'process_presentation_response',
            });
            throw error;
        }
    }
}

results matching ""

    No results matching ""