UNPKG

@sphereon/did-auth-siop

Version:

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

167 lines (144 loc) 6.28 kB
import { calculateJwkThumbprintUri, CustomJwtVerifier, DidJwtVerifier, getDidJwtVerifier, getDigestAlgorithmFromJwkThumbprintUri, getJwtVerifierWithContext as getJwtVerifierWithContextCommon, getX5cVerifier, JWK, JwkJwtVerifier as JwkJwtVerifierBase, JwtHeader, JwtPayload, JwtType, OpenIdFederationJwtVerifier, parseJWT, VerifyJwtCallbackBase, X5cJwtVerifier, } from '@sphereon/oid4vc-common' import SIOPErrors from './Errors' import { RequestObjectPayload } from './SIOP.types' type JwkJwtVerifier = | (JwkJwtVerifierBase & { type: 'id-token' jwkThumbprint: string }) | (JwkJwtVerifierBase & { type: 'request-object' | 'verifier-attestation' | 'dpop' jwkThumbprint?: never }) export type JwtVerifier = DidJwtVerifier | X5cJwtVerifier | CustomJwtVerifier | JwkJwtVerifier | OpenIdFederationJwtVerifier export const getJwkVerifier = async ( jwt: { header: JwtHeader; payload: JwtPayload }, jwkJwtVerifier: JwkJwtVerifierBase, ): Promise<JwkJwtVerifier> => { if (jwkJwtVerifier.type !== 'id-token') { // TODO: check why ts is complaining if we return the jwkJwtVerifier directly return { ...jwkJwtVerifier, type: jwkJwtVerifier.type, } } if (typeof jwt.payload.sub_jwk !== 'string') { throw new Error(`${SIOPErrors.INVALID_JWT} '${jwkJwtVerifier.type}' missing sub_jwk claim.`) } const jwkThumbPrintUri = jwt.payload.sub_jwk const digestAlgorithm = await getDigestAlgorithmFromJwkThumbprintUri(jwkThumbPrintUri) const selfComputedJwkThumbPrintUri = await calculateJwkThumbprintUri(jwt.header.jwk as JWK, digestAlgorithm) if (selfComputedJwkThumbPrintUri !== jwkThumbPrintUri) { throw new Error(`${SIOPErrors.INVALID_JWT} '${jwkJwtVerifier.type}' contains an invalid sub_jwk claim.`) } return { ...jwkJwtVerifier, type: jwkJwtVerifier.type, jwkThumbprint: jwt.payload.sub_jwk } } export const getJwtVerifierWithContext = async ( jwt: { header: JwtHeader; payload: JwtPayload }, options: { type: JwtType }, ): Promise<JwtVerifier> => { const verifierWithContext = await getJwtVerifierWithContextCommon(jwt, options) if (verifierWithContext.method === 'jwk') { return getJwkVerifier(jwt, verifierWithContext) } return verifierWithContext } export const getRequestObjectJwtVerifier = async ( jwt: { header: JwtHeader; payload: RequestObjectPayload }, options: { raw: string }, ): Promise<JwtVerifier> => { const type = 'request-object' const clientIdScheme = jwt.payload.client_id_scheme const clientId = jwt.payload.client_id if (!clientIdScheme || jwt.header.alg === 'none') { return getJwtVerifierWithContext(jwt, { type }) } if (clientIdScheme === 'did') { return getDidJwtVerifier(jwt, { type }) } else if (clientIdScheme === 'pre-registered') { // All validations must be done manually // The Verifier metadata is obtained using [RFC7591] or through out-of-band mechanisms. return getJwtVerifierWithContext(jwt, { type }) } else if (clientIdScheme === 'x509_san_dns' || clientIdScheme === 'x509_san_uri') { return getX5cVerifier(jwt, { type }) } else if (clientIdScheme === 'redirect_uri') { if (jwt.payload.redirect_uri && jwt.payload.redirect_uri !== clientId) { throw new Error(SIOPErrors.INVALID_CLIENT_ID_MUST_MATCH_REDIRECT_URI) } else if (jwt.payload.response_uri && jwt.payload.response_uri !== clientId) { throw new Error(SIOPErrors.INVALID_CLIENT_ID_MUST_MATCH_RESPONSE_URI) } /*const parts = options.raw.split('.') this can be signed and execution can't even be here when alg = none if (parts.length > 2 && parts[2]) { throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' JWT must not be signed`) }*/ return getJwtVerifierWithContext(jwt, { type }) } else if (clientIdScheme === 'verifier_attestation') { const verifierAttestationSubtype = 'verifier-attestation+jwt' if (!jwt.header.jwt) { throw new Error(SIOPErrors.MISSING_ATTESTATION_JWT_WITH_CLIENT_ID_SCHEME_ATTESTATION) } // TODO: is this correct? not 100% sure based on the spec if (jwt.header.typ !== verifierAttestationSubtype) { throw new Error(SIOPErrors.MISSING_ATTESTATION_JWT_TYP) } const attestationJwt = jwt.header.jwt const { header: attestationHeader, payload: attestationPayload } = parseJWT(attestationJwt) if ( attestationHeader.typ !== verifierAttestationSubtype || attestationPayload.sub !== clientId || !attestationPayload.iss || typeof attestationPayload.iss !== 'string' || !attestationPayload.exp || typeof attestationPayload.exp !== 'number' || typeof attestationPayload.cnf !== 'object' || !attestationPayload.cnf || !('jwk' in attestationPayload.cnf) || typeof attestationPayload.cnf['jwk'] !== 'object' ) { throw new Error(SIOPErrors.BAD_VERIFIER_ATTESTATION) } if (attestationPayload.redirect_uris) { if ( !Array.isArray(attestationPayload.redirect_uris) || attestationPayload.redirect_uris.some((value) => typeof value !== 'string') || !jwt.payload.redirect_uri || !attestationPayload.redirect_uris.includes(jwt.payload.redirect_uri) ) { throw new Error(SIOPErrors.BAD_VERIFIER_ATTESTATION_REDIRECT_URIS) } } const jwk = attestationPayload.cnf['jwk'] as JWK const alg = jwk.alg ?? attestationHeader.alg if (!alg) { throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' JWT header is missing alg.`) } // The iss claim value of the Verifier Attestation JWT MUST identify a party the Wallet trusts for issuing Verifier Attestation JWTs. // If the Wallet cannot establish trust, it MUST refuse the request. return { method: 'jwk', type, jwk: attestationPayload.cnf['jwk'] as JWK, alg } } else if (clientIdScheme === 'entity_id') { const entityId = jwt.payload.entity_id if (!entityId || !entityId.startsWith('https')) { throw new Error(SIOPErrors.INVALID_REQUEST_OBJECT_ENTITY_ID_SCHEME_CLIENT_ID) } return { method: 'openid-federation', type, entityId } } throw new Error(SIOPErrors.INVALID_CLIENT_ID_SCHEME) } export type VerifyJwtCallback = VerifyJwtCallbackBase<JwtVerifier>