UNPKG

@sphereon/did-auth-siop

Version:

Self Issued OpenID V2 (SIOPv2) and OpenID 4 Verifiable Presentations (OID4VP)

279 lines (242 loc) 11.5 kB
import { CredentialMapper, Hasher, WrappedVerifiablePresentation } from '@sphereon/ssi-types' import { DcqlPresentation } from 'dcql' import { AuthorizationRequest, VerifyAuthorizationRequestOpts } from '../authorization-request' import { assertValidVerifyAuthorizationRequestOpts } from '../authorization-request/Opts' import { IDToken } from '../id-token' import { AuthorizationResponsePayload, ResponseType, SIOPErrors, VerifiedAuthorizationRequest, VerifiedAuthorizationResponse } from '../types' import { Dcql } from './Dcql' import { assertValidVerifiablePresentations, extractNonceFromWrappedVerifiablePresentation, extractPresentationsFromDcqlVpToken, extractPresentationsFromVpToken, verifyPresentations, } from './OpenID4VP' import { assertValidResponseOpts } from './Opts' import { createResponsePayload } from './Payload' import { AuthorizationResponseOpts, PresentationDefinitionWithLocation, VerifyAuthorizationResponseOpts } from './types' export class AuthorizationResponse { private readonly _authorizationRequest?: AuthorizationRequest | undefined // private _requestObject?: RequestObject | undefined private readonly _idToken?: IDToken private readonly _payload: AuthorizationResponsePayload private readonly _options?: AuthorizationResponseOpts private constructor({ authorizationResponsePayload, idToken, responseOpts, authorizationRequest, }: { authorizationResponsePayload: AuthorizationResponsePayload idToken?: IDToken responseOpts?: AuthorizationResponseOpts authorizationRequest?: AuthorizationRequest }) { this._authorizationRequest = authorizationRequest this._options = responseOpts this._idToken = idToken this._payload = authorizationResponsePayload } /** * Creates a SIOP Response Object * * @param requestObject * @param responseOpts * @param verifyOpts */ static async fromRequestObject( requestObject: string, responseOpts: AuthorizationResponseOpts, verifyOpts: VerifyAuthorizationRequestOpts, ): Promise<AuthorizationResponse> { assertValidVerifyAuthorizationRequestOpts(verifyOpts) assertValidResponseOpts(responseOpts) if (!requestObject || !requestObject.startsWith('ey')) { throw new Error(SIOPErrors.NO_JWT) } const authorizationRequest = await AuthorizationRequest.fromUriOrJwt(requestObject) return AuthorizationResponse.fromAuthorizationRequest(authorizationRequest, responseOpts, verifyOpts) } static async fromPayload( authorizationResponsePayload: AuthorizationResponsePayload, responseOpts?: AuthorizationResponseOpts, ): Promise<AuthorizationResponse> { if (!authorizationResponsePayload) { throw new Error(SIOPErrors.NO_RESPONSE) } if (responseOpts) { assertValidResponseOpts(responseOpts) } const idToken = authorizationResponsePayload.id_token ? await IDToken.fromIDToken(authorizationResponsePayload.id_token) : undefined return new AuthorizationResponse({ authorizationResponsePayload, idToken, responseOpts, }) } static async fromAuthorizationRequest( authorizationRequest: AuthorizationRequest, responseOpts: AuthorizationResponseOpts, verifyOpts: VerifyAuthorizationRequestOpts, ): Promise<AuthorizationResponse> { assertValidResponseOpts(responseOpts) if (!authorizationRequest) { throw new Error(SIOPErrors.NO_REQUEST) } const verifiedRequest = await authorizationRequest.verify(verifyOpts) return await AuthorizationResponse.fromVerifiedAuthorizationRequest(verifiedRequest, responseOpts, verifyOpts) } static async fromVerifiedAuthorizationRequest( verifiedAuthorizationRequest: VerifiedAuthorizationRequest, responseOpts: AuthorizationResponseOpts, verifyOpts: VerifyAuthorizationRequestOpts, ): Promise<AuthorizationResponse> { assertValidResponseOpts(responseOpts) if (!verifiedAuthorizationRequest) { throw new Error(SIOPErrors.NO_REQUEST) } const authorizationRequest = verifiedAuthorizationRequest.authorizationRequest // const merged = verifiedAuthorizationRequest.authorizationRequest.requestObject, verifiedAuthorizationRequest.requestObject); // const presentationDefinitions = await PresentationExchange.findValidPresentationDefinitions(merged, await authorizationRequest.getSupportedVersion()); const presentationDefinitions = JSON.parse( JSON.stringify(verifiedAuthorizationRequest.presentationDefinitions), ) as PresentationDefinitionWithLocation[] const wantsIdToken = await authorizationRequest.containsResponseType(ResponseType.ID_TOKEN) const hasVpToken = await authorizationRequest.containsResponseType(ResponseType.VP_TOKEN) const idToken = wantsIdToken ? await IDToken.fromVerifiedAuthorizationRequest(verifiedAuthorizationRequest, responseOpts) : undefined const idTokenPayload = idToken ? await idToken.payload() : undefined const authorizationResponsePayload = await createResponsePayload(authorizationRequest, responseOpts, idTokenPayload) const response = new AuthorizationResponse({ authorizationResponsePayload, idToken, responseOpts, authorizationRequest, }) if (!hasVpToken) return response if (responseOpts.presentationExchange) { const wrappedPresentations = response.payload.vp_token ? extractPresentationsFromVpToken(response.payload.vp_token, { hasher: verifyOpts.hasher, }) : [] await assertValidVerifiablePresentations({ presentationDefinitions, presentations: wrappedPresentations, verificationCallback: verifyOpts.verification.presentationVerificationCallback, opts: { ...responseOpts.presentationExchange, hasher: verifyOpts.hasher, }, }) } else if (verifiedAuthorizationRequest.dcqlQuery) { await Dcql.assertValidDcqlPresentationResult( responseOpts.dcqlResponse.dcqlPresentation as DcqlPresentation, verifiedAuthorizationRequest.dcqlQuery, { hasher: verifyOpts.hasher, }, ) } else { throw new Error('vp_token is present, but no presentation definitions or dcql query provided') } return response } public async verify(verifyOpts: VerifyAuthorizationResponseOpts): Promise<VerifiedAuthorizationResponse> { // Merge payloads checks for inconsistencies in properties which are present in both the auth request and request object const merged = await this.mergedPayloads({ consistencyCheck: true, hasher: verifyOpts.hasher, }) if (verifyOpts.state && merged.state !== verifyOpts.state) { throw Error(SIOPErrors.BAD_STATE) } const verifiedIdToken = await this.idToken?.verify(verifyOpts) if (this.payload.vp_token && !verifyOpts.presentationDefinitions && !verifyOpts.dcqlQuery) { return Promise.reject(Error('vp_token is present, but no presentation definitions or dcql query provided')) } const emptyPresentationDefinitions = Array.isArray(verifyOpts.presentationDefinitions) && verifyOpts.presentationDefinitions.length === 0 if (!this.payload.vp_token && ((verifyOpts.presentationDefinitions && !emptyPresentationDefinitions) || verifyOpts.dcqlQuery)) { return Promise.reject(Error('Presentation definitions or dcql query provided, but no vp_token present')) } const oid4vp = this.payload.vp_token ? await verifyPresentations(this, verifyOpts) : undefined // Gather all nonces const allNonces = new Set<string>() if (oid4vp && (oid4vp.dcql?.nonce || oid4vp.presentationExchange?.nonce)) allNonces.add(oid4vp.dcql?.nonce ?? oid4vp.presentationExchange?.nonce) if (verifiedIdToken) allNonces.add(verifiedIdToken.payload.nonce) if (merged.nonce) allNonces.add(merged.nonce) // We only verify the nonce if there is one. We handle the case if the nonce is undefined // but it should be defined elsewhere. So if the nonce is undefined we don't have to verify it const firstNonce = Array.from(allNonces)[0] if (allNonces.size > 1) { throw new Error('both id token and VPs in vp token if present must have a nonce, and all nonces must be the same') } if (verifyOpts.nonce && firstNonce && firstNonce !== verifyOpts.nonce) { throw Error(SIOPErrors.BAD_NONCE) } const state = merged.state ?? verifiedIdToken?.payload.state if (!state) { throw Error('State is required') } return { authorizationResponse: this, verifyOpts, nonce: firstNonce, state, correlationId: verifyOpts.correlationId, ...(this.idToken && { idToken: verifiedIdToken }), ...(oid4vp?.presentationExchange && { oid4vpSubmission: oid4vp.presentationExchange }), ...(oid4vp?.dcql && { oid4vpSubmissionDcql: oid4vp.dcql }), } } get authorizationRequest(): AuthorizationRequest | undefined { return this._authorizationRequest } get payload(): AuthorizationResponsePayload { return this._payload } get options(): AuthorizationResponseOpts | undefined { return this._options } get idToken(): IDToken | undefined { return this._idToken } public getMergedProperty<T>(key: string, opts?: { consistencyCheck?: boolean; hasher?: Hasher }): T | undefined { const merged = this.mergedPayloads(opts) // FIXME this is really bad, expensive... return merged[key] as T } public mergedPayloads(opts?: { consistencyCheck?: boolean; hasher?: Hasher }): AuthorizationResponsePayload { let nonce: string | undefined = this._payload.nonce if (this._payload?.vp_token) { let presentations: WrappedVerifiablePresentation | WrappedVerifiablePresentation[] try { presentations = extractPresentationsFromDcqlVpToken(this._payload.vp_token as string, opts) } catch (e) { presentations = extractPresentationsFromVpToken(this._payload.vp_token, opts) } if (!presentations || (Array.isArray(presentations) && presentations.length === 0)) { return Promise.reject(Error('missing presentation(s)')) } const presentationsArray = Array.isArray(presentations) ? presentations : [presentations] // We do not verify them, as that is done elsewhere. So we simply can take the first nonce nonce = presentationsArray // FIXME toWrappedVerifiablePresentation() does not extract the nonce yet from mdocs. // However the nonce is validated as part of the mdoc verification process (using the session transcript bytes) // Once it is available we can also test it here, but it will be verified elsewhre as well .filter((presentation) => !CredentialMapper.isWrappedMdocPresentation(presentation)) .map(extractNonceFromWrappedVerifiablePresentation) .find((nonce) => nonce !== undefined) } const idTokenPayload = this.idToken?.payload() if (opts?.consistencyCheck !== false && idTokenPayload) { Object.entries(idTokenPayload).forEach((entry) => { if (typeof entry[0] === 'string' && this.payload[entry[0]] && this.payload[entry[0]] !== entry[1]) { throw Error(`Mismatch in Authorization Request and Request object value for ${entry[0]}`) } }) } if (!nonce && this._idToken) { nonce = idTokenPayload.nonce } return { ...this.payload, ...idTokenPayload, nonce } } }