UNPKG

@sphereon/did-auth-siop

Version:

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

233 lines (202 loc) 8.48 kB
import { calculateJwkThumbprintUri, JwtHeader, JwtIssuer, parseJWT } from '@sphereon/oid4vc-common' import { AuthorizationResponseOpts, VerifyAuthorizationResponseOpts } from '../authorization-response' import { assertValidVerifyOpts } from '../authorization-response/Opts' import { getJwtVerifierWithContext, IDTokenJwt, IDTokenPayload, JWK, JWTPayload, ResponseIss, SIOPErrors, VerifiedAuthorizationRequest, VerifiedIDToken, } from '../types' import { JwtIssuerWithContext } from '../types/VpJwtIssuer' import { createIDTokenPayload } from './Payload' export class IDToken { private _header?: JwtHeader private _payload?: IDTokenPayload private _jwt?: IDTokenJwt private readonly _responseOpts: AuthorizationResponseOpts private constructor(jwt?: IDTokenJwt, payload?: IDTokenPayload, responseOpts?: AuthorizationResponseOpts) { this._jwt = jwt this._payload = payload this._responseOpts = responseOpts } public static async fromVerifiedAuthorizationRequest( verifiedAuthorizationRequest: VerifiedAuthorizationRequest, responseOpts: AuthorizationResponseOpts, verifyOpts?: VerifyAuthorizationResponseOpts, ) { const authorizationRequestPayload = verifiedAuthorizationRequest.authorizationRequestPayload if (!authorizationRequestPayload) { throw new Error(SIOPErrors.NO_REQUEST) } const idToken = new IDToken(null, await createIDTokenPayload(verifiedAuthorizationRequest, responseOpts), responseOpts) if (verifyOpts) { await idToken.verify(verifyOpts) } return idToken } public static async fromIDToken(idTokenJwt: IDTokenJwt, verifyOpts?: VerifyAuthorizationResponseOpts) { if (!idTokenJwt) { throw new Error(SIOPErrors.NO_JWT) } const idToken = new IDToken(idTokenJwt, undefined) if (verifyOpts) { await idToken.verify(verifyOpts) } return idToken } public static async fromIDTokenPayload( idTokenPayload: IDTokenPayload, responseOpts: AuthorizationResponseOpts, verifyOpts?: VerifyAuthorizationResponseOpts, ) { if (!idTokenPayload) { throw new Error(SIOPErrors.NO_JWT) } const idToken = new IDToken(null, idTokenPayload, responseOpts) if (verifyOpts) { await idToken.verify(verifyOpts) } return idToken } public payload(): IDTokenPayload { if (!this._payload) { if (!this._jwt) { throw new Error(SIOPErrors.NO_JWT) } const { header, payload } = this.parseAndVerifyJwt() this._header = header this._payload = payload } return this._payload } public async jwt(_jwtIssuer: JwtIssuer): Promise<IDTokenJwt> { if (!this._jwt) { if (!this.responseOpts) { throw Error(SIOPErrors.BAD_IDTOKEN_RESPONSE_OPTS) } const jwtIssuer: JwtIssuerWithContext = _jwtIssuer ? { ..._jwtIssuer, type: 'id-token', authorizationResponseOpts: this.responseOpts } : { method: 'custom', type: 'id-token', authorizationResponseOpts: this.responseOpts } if (jwtIssuer.method === 'custom') { this._jwt = await this.responseOpts.createJwtCallback(jwtIssuer, { header: {}, payload: this._payload }) } else if (jwtIssuer.method === 'did') { const did = jwtIssuer.didUrl.split('#')[0] this._payload.sub = did const issuer = this._responseOpts.registration?.issuer || this._payload.iss if (!issuer || !(issuer.includes(ResponseIss.SELF_ISSUED_V2) || issuer === this._payload.sub)) { throw new Error(SIOPErrors.NO_SELF_ISSUED_ISS) } if (!this._payload.iss) { this._payload.iss = issuer } const header = { kid: jwtIssuer.didUrl, alg: jwtIssuer.alg, typ: 'JWT' } this._jwt = await this.responseOpts.createJwtCallback({ ...jwtIssuer, type: 'id-token' }, { header, payload: this._payload }) } else if (jwtIssuer.method === 'x5c') { this._payload.iss = jwtIssuer.issuer this._payload.sub = jwtIssuer.issuer const header = { x5c: jwtIssuer.x5c, typ: 'JWT' } this._jwt = await this._responseOpts.createJwtCallback(jwtIssuer, { header, payload: this._payload }) } else if (jwtIssuer.method === 'jwk') { const jwkThumbprintUri = await calculateJwkThumbprintUri(jwtIssuer.jwk as JWK) this._payload.sub = jwkThumbprintUri this._payload.iss = jwkThumbprintUri this._payload.sub_jwk = jwtIssuer.jwk const header = { jwk: jwtIssuer.jwk, alg: jwtIssuer.jwk.alg, typ: 'JWT' } this._jwt = await this._responseOpts.createJwtCallback(jwtIssuer, { header, payload: this._payload }) } else { throw new Error(`JwtIssuer method '${(jwtIssuer as JwtIssuer).method}' not implemented`) } const { header, payload } = this.parseAndVerifyJwt() this._header = header this._payload = payload } return this._jwt } private parseAndVerifyJwt(): { header: JwtHeader; payload: IDTokenPayload } { const { header, payload } = parseJWT(this._jwt) this.assertValidResponseJWT({ header, payload }) const idTokenPayload = payload as IDTokenPayload return { header, payload: idTokenPayload } } /** * Verifies a SIOP ID Response JWT on the RP Side * * @param idToken ID token to be validated * @param verifyOpts */ public async verify(verifyOpts: VerifyAuthorizationResponseOpts): Promise<VerifiedIDToken> { assertValidVerifyOpts(verifyOpts) if (!this._jwt) { throw new Error(SIOPErrors.NO_JWT) } const parsedJwt = parseJWT(this._jwt) this.assertValidResponseJWT(parsedJwt) const idTokenPayload = parsedJwt.payload as IDTokenPayload const jwtVerifier = await getJwtVerifierWithContext(parsedJwt, { type: 'id-token' }) const verificationResult = await verifyOpts.verifyJwtCallback(jwtVerifier, { ...parsedJwt, raw: this._jwt }) if (!verificationResult) { throw Error(SIOPErrors.ERROR_VERIFYING_SIGNATURE) } this.assertValidResponseJWT({ header: parsedJwt.header, verPayload: idTokenPayload, audience: verifyOpts.audience }) // Enforces verifyPresentationCallback function on the RP side, if (!verifyOpts?.verification.presentationVerificationCallback) { throw new Error(SIOPErrors.VERIFIABLE_PRESENTATION_VERIFICATION_FUNCTION_MISSING) } return { jwt: this._jwt, payload: { ...idTokenPayload }, verifyOpts, } } static async verify(idTokenJwt: IDTokenJwt, verifyOpts: VerifyAuthorizationResponseOpts): Promise<VerifiedIDToken> { const idToken = await IDToken.fromIDToken(idTokenJwt, verifyOpts) const verifiedIdToken = await idToken.verify(verifyOpts) return { ...verifiedIdToken, } } private assertValidResponseJWT(opts: { header: JwtHeader; payload?: JWTPayload; verPayload?: IDTokenPayload; audience?: string; nonce?: string }) { if (!opts.header) { throw new Error(SIOPErrors.BAD_PARAMS) } if (opts.payload) { if (!opts.payload.iss || !(opts.payload.iss.includes(ResponseIss.SELF_ISSUED_V2) || opts.payload.iss.startsWith('did:'))) { throw new Error(`${SIOPErrors.NO_SELF_ISSUED_ISS}, got: ${opts.payload.iss}`) } } if (opts.verPayload) { if (!opts.verPayload.nonce) { throw Error(SIOPErrors.NO_NONCE) // No need for our own expiration check. DID jwt already does that /*} else if (!opts.verPayload.exp || opts.verPayload.exp < Date.now() / 1000) { throw Error(SIOPErrors.EXPIRED); /!*} else if (!opts.verPayload.iat || opts.verPayload.iat > (Date.now() / 1000)) { throw Error(SIOPErrors.EXPIRED);*!/ // todo: Add iat check */ } if ((opts.verPayload.aud && !opts.audience) || (!opts.verPayload.aud && opts.audience)) { throw Error(SIOPErrors.NO_AUDIENCE) } else if (opts.audience && opts.audience != opts.verPayload.aud) { throw Error(SIOPErrors.INVALID_AUDIENCE) } else if (opts.nonce && opts.nonce != opts.verPayload.nonce) { throw Error(SIOPErrors.BAD_NONCE) } } } get header(): JwtHeader { return this._header } get responseOpts(): AuthorizationResponseOpts { return this._responseOpts } public async isSelfIssued(): Promise<boolean> { const payload = await this.payload() return payload.iss === ResponseIss.SELF_ISSUED_V2 || (payload.sub !== undefined && payload.sub === payload.iss) } }