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 'src/utils/webhook.dto';

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,
    ) {}

    async createAuthorizationRequest(
        requestId: string,
        auth_session?: string,
    ): Promise<string> {
        const host = this.configService
            .getOrThrow<string>('PUBLIC_URL')
            .replace('https://', '');
        const values =
            await this.presentationsService.getPresentationRequest(requestId);
        const regCert = await this.registrarService.addRegistrationCertificate(
            values.registrationCert,
            values.dcql_query,
            requestId,
        );
        if (!auth_session) {
            auth_session = v4();
            await this.sessionService.create({ id: auth_session });
        }
        const nonce = randomUUID();
        await this.sessionService.add(auth_session, { vp_nonce: nonce });

        const request = {
            payload: {
                response_type: 'vp_token',
                client_id: 'x509_san_dns:' + host,
                response_uri: 'https://' + host + '/oid4vp/response',
                response_mode: 'direct_post.jwt',
                nonce,
                dcql_query: values.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>(
                            'REGISTRAR_RP_NAME',
                        ),
                    response_types_supported: ['vp_token'],
                },
                state: auth_session,
                aud: 'https://' + host,
                exp: Math.floor(Date.now() / 1000) + 60 * 5,
                iat: Math.floor(new Date().getTime() / 1000),
                verifier_attestations: [
                    {
                        format: 'jwt',
                        data: regCert,
                    },
                ],
            },
            header: {
                typ: 'oauth-authz-req+jwt',
            },
        };

        const header = {
            ...request.header,
            alg: 'ES256',
            x5c: this.cryptoService.getCertChain('access'),
        };

        return this.cryptoService.signJwt(header, request.payload);
    }

    async createRequest(
        requestId: string,
        values: PresentationRequestOptions,
    ): Promise<OfferResponse> {
        const vpRequest =
            await this.presentationsService.getPresentationRequest(requestId);

        if (!values.session) {
            values.session = v4();
            await this.sessionService.create({
                id: values.session,
                webhook: values.webhook ?? vpRequest.webhook,
            });
        } else {
            await this.sessionService.add(values.session, {
                webhook: values.webhook ?? vpRequest.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')}/oid4vp/request/${requestId}/${values.session}`,
        };
        const queryString = Object.entries(params)
            .map(
                ([key, value]) =>
                    `${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
            )
            .join('&');

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

    async getResponse(body: AuthorizationResponse) {
        const res = await this.encryptionService.decryptJwe<AuthResponse>(
            body.response,
        );
        const session = await this.sessionService.get(res.state);
        //TODO: load required fields from the config
        const credentials = await this.presentationsService.parseResponse(
            res,
            [],
            session.vp_nonce as string,
        );
        //tell the auth server the result of the session.
        await this.sessionService.add(res.state, {
            //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.auhth &&
                session.webhook.auhth.type === 'apiKey'
            ) {
                headers[session.webhook.auhth.config.headerName] =
                    session.webhook.auhth.config.value;
            }
            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, {
                            credentialPayload: session.credentialPayload,
                        });
                    }
                },
                (err) => {
                    console.error('Error sending webhook:', err);
                    throw new Error(
                        `Error sending webhook: ${err.message || err}`,
                    );
                },
            );
        }
    }
}

results matching ""

    No results matching ""