UNPKG

@sphereon/did-auth-siop

Version:

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

174 lines (153 loc) 8.13 kB
import { JwtHeader, JwtIssuer, parseJWT } from '@sphereon/oid4vc-common' import { ClaimPayloadCommonOpts, ClaimPayloadOptsVID1, CreateAuthorizationRequestOpts } from '../authorization-request' import { assertValidAuthorizationRequestOpts } from '../authorization-request/Opts' import { fetchByReferenceOrUseByValue, removeNullUndefined } from '../helpers' import { AuthorizationRequestPayload, JwtIssuerWithContext, RequestObjectJwt, RequestObjectPayload, SIOPErrors } from '../types' import { assertValidRequestObjectOpts } from './Opts' import { assertValidRequestObjectPayload, createRequestObjectPayload } from './Payload' import { RequestObjectOpts } from './types' export class RequestObject { private payload: RequestObjectPayload private jwt?: RequestObjectJwt private readonly opts: RequestObjectOpts<ClaimPayloadCommonOpts | ClaimPayloadOptsVID1> private constructor( opts?: CreateAuthorizationRequestOpts | RequestObjectOpts<ClaimPayloadCommonOpts | ClaimPayloadOptsVID1>, payload?: RequestObjectPayload, jwt?: string, ) { this.opts = opts ? RequestObject.mergeOAuth2AndOpenIdProperties(opts) : undefined this.payload = payload this.jwt = jwt } /** * Create a request object that typically is used as a JWT on RP side, typically this method is called automatically when creating an Authorization Request, but you could use it directly! * * @param authorizationRequestOpts Request Object options to build a Request Object * @remarks This method is used to generate a SIOP request Object. * First it generates the request object payload, and then it a signed JWT can be accessed on request. * * Normally you will want to use the Authorization Request class. That class creates a URI that includes the JWT from this class in the URI * If you do use this class directly, you can call the `convertRequestObjectToURI` afterwards to get the URI. * Please note that the Authorization Request allows you to differentiate between OAuth2 and OpenID parameters that become * part of the URI and which become part of the Request Object. If you generate a URI based upon the result of this class, * the URI will be constructed based on the Request Object only! */ public static async fromOpts(authorizationRequestOpts: CreateAuthorizationRequestOpts): Promise<RequestObject> { assertValidAuthorizationRequestOpts(authorizationRequestOpts) const createJwtCallback = authorizationRequestOpts.requestObject.createJwtCallback // We copy the signature separately as it can contain a function, which would be removed in the merge function below const jwtIssuer: JwtIssuer = authorizationRequestOpts.requestObject.jwtIssuer // We copy the signature separately as it can contain a function, which would be removed in the merge function below const requestObjectOpts: RequestObjectOpts<ClaimPayloadCommonOpts> = RequestObject.mergeOAuth2AndOpenIdProperties(authorizationRequestOpts) const mergedOpts = { ...authorizationRequestOpts, requestObject: { ...authorizationRequestOpts.requestObject, ...requestObjectOpts, createJwtCallback, jwtIssuer }, } return new RequestObject(mergedOpts, await createRequestObjectPayload(mergedOpts)) } public static async fromJwt(requestObjectJwt: RequestObjectJwt): Promise<RequestObject | undefined> { return requestObjectJwt ? new RequestObject(undefined, undefined, requestObjectJwt) : undefined } public static async fromPayload( requestObjectPayload: RequestObjectPayload, authorizationRequestOpts: CreateAuthorizationRequestOpts, ): Promise<RequestObject> { return new RequestObject(authorizationRequestOpts, requestObjectPayload) } public static async fromAuthorizationRequestPayload(payload: AuthorizationRequestPayload): Promise<RequestObject | undefined> { const requestObjectJwt = (payload.request ?? payload.request_uri) ? await fetchByReferenceOrUseByValue(payload.request_uri as string, payload.request, true) : undefined return requestObjectJwt ? await RequestObject.fromJwt(requestObjectJwt) : undefined } public async toJwt(): Promise<RequestObjectJwt | undefined> { if (!this.jwt) { if (!this.opts) { throw Error(SIOPErrors.BAD_PARAMS) } else if (!this.payload) { return undefined } this.removeRequestProperties() if (this.payload.registration_uri) { delete this.payload.registration } assertValidRequestObjectPayload(this.payload) const jwtIssuer: JwtIssuerWithContext = this.opts.jwtIssuer ? { ...this.opts.jwtIssuer, type: 'request-object' } : { method: 'custom', type: 'request-object' } if (jwtIssuer.method === 'custom') { this.jwt = await this.opts.createJwtCallback(jwtIssuer, { header: {}, payload: this.payload }) } else if (jwtIssuer.method === 'did') { const did = jwtIssuer.didUrl.split('#')[0] this.payload.iss = this.payload.iss ?? did this.payload.sub = this.payload.sub ?? did this.payload.client_id = this.payload.client_id ?? did this.payload.client_id_scheme = 'did' const header = { kid: jwtIssuer.didUrl, alg: jwtIssuer.alg, typ: 'JWT' } this.jwt = await this.opts.createJwtCallback(jwtIssuer, { header, payload: this.payload }) } else if (jwtIssuer.method === 'x5c') { this.payload.iss = jwtIssuer.issuer const header = { x5c: jwtIssuer.x5c, typ: 'JWT', alg: jwtIssuer.alg } this.jwt = await this.opts.createJwtCallback(jwtIssuer, { header, payload: this.payload }) } else if (jwtIssuer.method === 'jwk') { if (!this.payload.client_id) { throw new Error('Please provide a client_id for the RP') } const header = { jwk: jwtIssuer.jwk, typ: 'JWT', alg: jwtIssuer.jwk.alg as string } this.jwt = await this.opts.createJwtCallback(jwtIssuer, { header, payload: this.payload }) } else { throw new Error(`JwtIssuer method '${(jwtIssuer as JwtIssuer).method}' not implemented`) } } return this.jwt } public getPayload(): RequestObjectPayload | undefined { if (!this.payload) { if (!this.jwt) { return undefined } this.payload = removeNullUndefined(parseJWT<JwtHeader, RequestObjectPayload>(this.jwt).payload) this.removeRequestProperties() if (this.payload.registration_uri) { delete this.payload.registration } else if (this.payload.registration) { delete this.payload.registration_uri } } assertValidRequestObjectPayload(this.payload) return this.payload } public async assertValid(): Promise<void> { if (this.options) { assertValidRequestObjectOpts(this.options, false) } assertValidRequestObjectPayload(await this.getPayload()) } public get options(): RequestObjectOpts<ClaimPayloadCommonOpts | ClaimPayloadOptsVID1> | undefined { return this.opts } private removeRequestProperties(): void { if (this.payload) { // https://openid.net/specs/openid-connect-core-1_0.html#RequestObject // request and request_uri parameters MUST NOT be included in Request Objects. delete this.payload.request delete this.payload.request_uri } } private static mergeOAuth2AndOpenIdProperties( opts: CreateAuthorizationRequestOpts | RequestObjectOpts<ClaimPayloadCommonOpts | ClaimPayloadOptsVID1>, ): RequestObjectOpts<ClaimPayloadCommonOpts | ClaimPayloadOptsVID1> { if (!opts) { throw Error(SIOPErrors.BAD_PARAMS) } const isAuthReq = opts['requestObject'] !== undefined const mergedOpts = JSON.parse(JSON.stringify(opts)) const createJwtCallback = opts['requestObject']?.createJwtCallback if (createJwtCallback) { mergedOpts.requestObject.createJwtCallback = createJwtCallback } const jwtIssuer = opts['requestObject']?.jwtIssuer if (createJwtCallback) { mergedOpts.requestObject.jwtIssuer = jwtIssuer } delete mergedOpts?.request?.requestObject return isAuthReq ? mergedOpts.requestObject : mergedOpts } }