diff --git a/packages/siop-oid4vp/lib/authorization-request/AuthorizationRequest.ts b/packages/siop-oid4vp/lib/authorization-request/AuthorizationRequest.ts index baad622f..79972996 100644 --- a/packages/siop-oid4vp/lib/authorization-request/AuthorizationRequest.ts +++ b/packages/siop-oid4vp/lib/authorization-request/AuthorizationRequest.ts @@ -30,8 +30,8 @@ import { CreateAuthorizationRequestOpts, VerifyAuthorizationRequestOpts } from ' export class AuthorizationRequest { private readonly _requestObject?: RequestObject private readonly _payload: AuthorizationRequestPayload - private readonly _options: CreateAuthorizationRequestOpts - private _uri: URI + private readonly _options: CreateAuthorizationRequestOpts | undefined + private _uri: URI | undefined private constructor(payload: AuthorizationRequestPayload, requestObject?: RequestObject, opts?: CreateAuthorizationRequestOpts, uri?: URI) { this._options = opts @@ -66,7 +66,7 @@ export class AuthorizationRequest { const requestObjectArg = opts.requestObject.passBy !== PassBy.NONE ? (requestObject ? requestObject : await RequestObject.fromOpts(opts)) : undefined - const requestPayload = opts?.payload ? await createAuthorizationRequestPayload(opts, requestObjectArg) : undefined + const requestPayload = await createAuthorizationRequestPayload(opts, requestObjectArg) return new AuthorizationRequest(requestPayload, requestObjectArg, opts) } @@ -116,10 +116,13 @@ export class AuthorizationRequest { async verify(opts: VerifyAuthorizationRequestOpts): Promise { assertValidVerifyAuthorizationRequestOpts(opts) - let requestObjectPayload: RequestObjectPayload + let requestObjectPayload: RequestObjectPayload | undefined const jwt = await this.requestObjectJwt() - const parsedJwt = jwt ? parseJWT(jwt) : undefined + if(jwt === undefined) { + return Promise.reject(Error('jwt could be fetched, request object unavailable')) + } + const parsedJwt = parseJWT(jwt) if (parsedJwt) { requestObjectPayload = parsedJwt.payload as RequestObjectPayload @@ -148,7 +151,7 @@ export class AuthorizationRequest { // AuthorizationRequest.assertValidRequestObject(origAuthenticationRequest); // We use the orig request for default values, but the JWT payload contains signed request object properties - const mergedPayload = { ...this.payload, ...requestObjectPayload } + const mergedPayload = { ...this.payload, ...(requestObjectPayload ? requestObjectPayload : {}) } if (opts.state && mergedPayload.state !== opts.state) { throw new Error(`${SIOPErrors.BAD_STATE} payload: ${mergedPayload.state}, supplied: ${opts.state}`) } else if (opts.nonce && mergedPayload.nonce !== opts.nonce) { @@ -164,7 +167,10 @@ export class AuthorizationRequest { ) assertValidRPRegistrationMedataPayload(registrationMetadataPayload) // TODO: We need to do something with the metadata probably + } else { + return Promise.reject(Error(`could not fetch registrationMetadataPayload due to missing payload key ${registrationPropertyKey}`)) } + // When the response_uri parameter is present, the redirect_uri Authorization Request parameter MUST NOT be present. If the redirect_uri Authorization Request parameter is present when the Response Mode is direct_post, the Wallet MUST return an invalid_request Authorization Response error. let responseURIType: ResponseURIType let responseURI: string @@ -220,7 +226,7 @@ export class AuthorizationRequest { throw Error(SIOPErrors.BAD_PARAMS) } const requestObject = await RequestObject.fromJwt(jwt) - const payload: AuthorizationRequestPayload = { ...(await requestObject.getPayload()) } as AuthorizationRequestPayload + const payload: AuthorizationRequestPayload = { ...(requestObject && await requestObject.getPayload()) } as AuthorizationRequestPayload // Although this was a RequestObject we instantiate it as AuthzRequest and then copy in the JWT as the request Object payload.request = jwt return new AuthorizationRequest({ ...payload }, requestObject) @@ -231,22 +237,23 @@ export class AuthorizationRequest { throw Error(SIOPErrors.BAD_PARAMS) } const uriObject = typeof uri === 'string' ? await URI.fromUri(uri) : uri - const requestObject = await RequestObject.fromJwt(uriObject.requestObjectJwt) + const requestObject = uriObject.requestObjectJwt ? await RequestObject.fromJwt(uriObject.requestObjectJwt) : undefined return new AuthorizationRequest(uriObject.authorizationRequestPayload, requestObject, undefined, uriObject) } public async toStateInfo(): Promise { - const requestObject = await this.requestObject.getPayload() + + const requestObjectPayload = this.requestObject !== undefined ? await this.requestObject.getPayload() : undefined return { - client_id: this.options.clientMetadata.client_id, - iat: requestObject.iat ?? this.payload.iat, - nonce: requestObject.nonce ?? this.payload.nonce, + client_id: this.options?.clientMetadata?.client_id ?? this.payload.client_id, + iat: requestObjectPayload?.iat ?? this.payload.iat, + nonce: requestObjectPayload?.nonce ?? this.payload.nonce, state: this.payload.state, } } public async containsResponseType(singleType: ResponseType | string): Promise { - const responseType: string = await this.getMergedProperty('response_type') + const responseType: string | undefined = await this.getMergedProperty('response_type') return responseType?.includes(singleType) === true } diff --git a/packages/siop-oid4vp/lib/authorization-response/PresentationExchange.ts b/packages/siop-oid4vp/lib/authorization-response/PresentationExchange.ts index a5d1cb59..726f3c6c 100644 --- a/packages/siop-oid4vp/lib/authorization-response/PresentationExchange.ts +++ b/packages/siop-oid4vp/lib/authorization-response/PresentationExchange.ts @@ -5,8 +5,9 @@ import { PresentationSubmissionLocation, SelectResults, Status, + Validated, VerifiablePresentationFromOpts, - VerifiablePresentationResult, + VerifiablePresentationResult } from '@sphereon/pex' import { PresentationEvaluationResults } from '@sphereon/pex/dist/main/lib/evaluation' import { Format, PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models' @@ -63,14 +64,14 @@ export class PresentationExchange { ...options, presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL, proofOptions: { - ...options.proofOptions, + ...options?.proofOptions, proofPurpose: options?.proofOptions?.proofPurpose ?? IProofPurpose.authentication, type: options?.proofOptions?.type ?? IProofType.EcdsaSecp256k1Signature2019, /* challenge: options?.proofOptions?.challenge, domain: options?.proofOptions?.domain,*/ }, signatureOptions: { - ...options.signatureOptions, + ...options?.signatureOptions, // verificationMethod: options?.signatureOptions?.verificationMethod, keyEncoding: options?.signatureOptions?.keyEncoding ?? KeyEncoding.Hex, }, @@ -116,9 +117,10 @@ export class PresentationExchange { } public static assertValidPresentationSubmission(presentationSubmission: PresentationSubmission) { - const validationResult = PEX.validateSubmission(presentationSubmission) - if (validationResult[0].message != 'ok') { - throw new Error(`${SIOPErrors.RESPONSE_OPTS_PRESENTATIONS_SUBMISSION_IS_NOT_VALID}, details ${JSON.stringify(validationResult[0])}`) + const validationResult:Validated = PEX.validateSubmission(presentationSubmission) + if (Array.isArray(validationResult) && validationResult[0].message != 'ok' + || !Array.isArray(validationResult) && validationResult.message != 'ok') { + throw new Error(`${SIOPErrors.RESPONSE_OPTS_PRESENTATIONS_SUBMISSION_IS_NOT_VALID}, details ${JSON.stringify(validationResult)}`) } } @@ -242,7 +244,8 @@ export class PresentationExchange { private static assertValidPresentationDefinition(presentationDefinition: IPresentationDefinition) { const validationResult = PEX.validateDefinition(presentationDefinition) - if (validationResult[0].message != 'ok') { + if (Array.isArray(validationResult) && validationResult[0].message != 'ok' + || !Array.isArray(validationResult) && validationResult.message != 'ok') { throw new Error(`${SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_NOT_VALID}`) } } @@ -317,7 +320,7 @@ export class PresentationExchange { throw new Error(SIOPErrors.NO_PRESENTATION_SUBMISSION) } - if (!evaluationResults.areRequiredCredentialsPresent || evaluationResults.errors.length || !evaluationResults.value) { + if (!evaluationResults.areRequiredCredentialsPresent || evaluationResults.errors || !evaluationResults.value) { throw new Error(`message: ${SIOPErrors.COULD_NOT_FIND_VCS_MATCHING_PD}, details: ${JSON.stringify(evaluationResults.errors)}`) } @@ -330,12 +333,12 @@ export class PresentationExchange { const presentationsToVerify = Array.isArray(evaluationResults.presentation) ? evaluationResults.presentation : [evaluationResults.presentation] // The verifyPresentationCallback function is mandatory for RP only, // So the behavior here is to bypass it if not present - if (verifyPresentationCallback) { + if (verifyPresentationCallback && evaluationResults.value !== undefined) { // Verify the signature of all VPs await Promise.all( presentationsToVerify.map(async (presentation) => { try { - const verificationResult = await verifyPresentationCallback(presentation as W3CVerifiablePresentation, evaluationResults.value) + const verificationResult = await verifyPresentationCallback(presentation as W3CVerifiablePresentation, evaluationResults.value!) if (!verificationResult.verified) { throw new Error( SIOPErrors.VERIFIABLE_PRESENTATION_SIGNATURE_NOT_VALID + verificationResult.reason ? `. ${verificationResult.reason}` : '', diff --git a/packages/siop-oid4vp/lib/helpers/Encodings.ts b/packages/siop-oid4vp/lib/helpers/Encodings.ts index 075def11..c7a56aa3 100644 --- a/packages/siop-oid4vp/lib/helpers/Encodings.ts +++ b/packages/siop-oid4vp/lib/helpers/Encodings.ts @@ -14,10 +14,11 @@ export function decodeUriAsJson(uri: string) { } const parts = parse(queryString, { plainObjects: true, depth: 10, parameterLimit: 5000, ignoreQueryPrefix: true }) - const descriptors = parts?.claims?.['vp_token']?.presentation_definition?.['input_descriptors'] + const vpToken = (parts?.claims as { [key: string]: any })?.['vp_token']; + const descriptors = vpToken?.presentation_definition?.['input_descriptors']; // FIXME? if (descriptors && Array.isArray(descriptors)) { // Whenever we have a [{'uri': 'str1'}, 'uri': 'str2'] qs changes this to {uri: ['str1','str2']} which means schema validation fails. So we have to fix that - parts.claims['vp_token'].presentation_definition['input_descriptors'] = descriptors.map((descriptor: InputDescriptorV1) => { + vpToken.presentation_definition['input_descriptors'] = descriptors.map((descriptor: InputDescriptorV1) => { if (Array.isArray(descriptor.schema)) { descriptor.schema = descriptor.schema.flatMap((val) => { if (typeof val === 'string') { @@ -32,7 +33,7 @@ export function decodeUriAsJson(uri: string) { }) } - const json = {} + const json:Record = {} for (const key in parts) { const value = parts[key] if (!value) { @@ -56,7 +57,7 @@ export function decodeUriAsJson(uri: string) { return JSON.parse(JSON.stringify(json)) } -export function encodeJsonAsURI(json: unknown, _opts?: { arraysWithIndex?: string[] }): string { +export function encodeJsonAsURI(json: Record, _opts?: { arraysWithIndex?: string[] }): string { if (typeof json === 'string') { return encodeJsonAsURI(JSON.parse(json)) } diff --git a/packages/siop-oid4vp/lib/helpers/HttpUtils.ts b/packages/siop-oid4vp/lib/helpers/HttpUtils.ts index a88df4ac..70ba748c 100644 --- a/packages/siop-oid4vp/lib/helpers/HttpUtils.ts +++ b/packages/siop-oid4vp/lib/helpers/HttpUtils.ts @@ -61,7 +61,7 @@ const siopFetch = async ( if (!url || url.toLowerCase().startsWith('did:')) { throw Error(`Invalid URL supplied. Expected a http(s) URL. Recieved: ${url}`) } - const headers = opts?.customHeaders ? opts.customHeaders : {} + const headers:Record = opts?.customHeaders ? opts.customHeaders : {} if (opts?.bearerToken) { headers['Authorization'] = `Bearer ${opts.bearerToken}` } @@ -87,7 +87,7 @@ const siopFetch = async ( const textResponseBody = await clonedResponse.text() const isJSONResponse = - (accept === 'application/json' || origResponse.headers['Content-Type'] === 'application/json') && textResponseBody.trim().startsWith('{') + (accept === 'application/json' || origResponse.headers.get('Content-Type') === 'application/json') && textResponseBody.trim().startsWith('{') const responseBody = isJSONResponse ? JSON.parse(textResponseBody) : textResponseBody if (success || opts?.exceptionOnHttpErrorStatus) { @@ -131,7 +131,7 @@ export const fetchByReferenceOrUseByValue = async (referenceURI: string, valu response = await getWithUrl(referenceURI, textResponse) } catch (e) { console.log(e) - throw new Error(`${SIOPErrors.REG_PASS_BY_REFERENCE_INCORRECTLY}: ${e.message}, URL: ${referenceURI}`) + throw new Error(`${SIOPErrors.REG_PASS_BY_REFERENCE_INCORRECTLY}: ${(e as Error).message}, URL: ${referenceURI}`) } } return response diff --git a/packages/siop-oid4vp/lib/helpers/LanguageTagUtils.ts b/packages/siop-oid4vp/lib/helpers/LanguageTagUtils.ts index 434512a4..c654940a 100644 --- a/packages/siop-oid4vp/lib/helpers/LanguageTagUtils.ts +++ b/packages/siop-oid4vp/lib/helpers/LanguageTagUtils.ts @@ -13,8 +13,8 @@ export class LanguageTagUtils { * * @param source is the object from which the language enabled fields and their values will be extracted. */ - static getAllLanguageTaggedProperties(source: unknown): Map { - return this.getLanguageTaggedPropertiesMapped(source, undefined) + static getAllLanguageTaggedProperties(source: object): Map { + return this.getLanguageTaggedPropertiesMapped(source, new Map() ) } /** @@ -23,7 +23,7 @@ export class LanguageTagUtils { * @param source is the object from which the language enabled fields and their values will be extracted. * @param requiredFieldNames the fields which are supposed to be language enabled. These are the only fields which should be returned. */ - static getLanguageTaggedProperties(source: unknown, requiredFieldNames: Array): Map { + static getLanguageTaggedProperties(source: object, requiredFieldNames: Array): Map { const languageTagEnabledFieldsNamesMapping: Map = new Map() requiredFieldNames.forEach((value) => languageTagEnabledFieldsNamesMapping.set(value, value)) return this.getLanguageTaggedPropertiesMapped(source, languageTagEnabledFieldsNamesMapping) @@ -36,7 +36,7 @@ export class LanguageTagUtils { * @param requiredFieldNamesMapping the fields which are supposed to be language enabled. These are the only fields which should be returned. And * the fields names will be transformed as per the mapping provided. */ - static getLanguageTaggedPropertiesMapped(source: unknown, requiredFieldNamesMapping: Map): Map { + static getLanguageTaggedPropertiesMapped(source: object, requiredFieldNamesMapping: Map): Map { this.assertSourceIsWorthChecking(source) this.assertValidTargetFieldNames(requiredFieldNamesMapping) diff --git a/packages/siop-oid4vp/lib/helpers/Metadata.ts b/packages/siop-oid4vp/lib/helpers/Metadata.ts index 5fa7e8d1..c27f2624 100644 --- a/packages/siop-oid4vp/lib/helpers/Metadata.ts +++ b/packages/siop-oid4vp/lib/helpers/Metadata.ts @@ -13,9 +13,9 @@ export function assertValidMetadata(opMetadata: DiscoveryMetadataPayload, rpMeta const credentials = supportedCredentialsFormats(rpMetadata.vp_formats, opMetadata.vp_formats) const isValidSubjectSyntax = verifySubjectSyntaxes(rpMetadata.subject_syntax_types_supported) if (isValidSubjectSyntax && rpMetadata.subject_syntax_types_supported) { - subjectSyntaxTypesSupported = supportedSubjectSyntaxTypes(rpMetadata.subject_syntax_types_supported, opMetadata.subject_syntax_types_supported) + subjectSyntaxTypesSupported = supportedSubjectSyntaxTypes(rpMetadata.subject_syntax_types_supported, opMetadata.subject_syntax_types_supported as string[]) } else if (isValidSubjectSyntax && (!rpMetadata.subject_syntax_types_supported || !rpMetadata.subject_syntax_types_supported.length)) { - if (opMetadata.subject_syntax_types_supported || opMetadata.subject_syntax_types_supported.length) { + if (opMetadata.subject_syntax_types_supported) { subjectSyntaxTypesSupported = [...opMetadata.subject_syntax_types_supported] } } @@ -84,21 +84,44 @@ function supportedSubjectSyntaxTypes(rpMethods: string[] | string, opMethods: st return supportedSubjectSyntaxTypes } +export function collectAlgValues(o: any): string[] { + const algValues: string[] = []; + for (const value of Object.values(o)) { + if (value) { + // Check if the object has an 'alg' property that's an array of strings + if (Array.isArray((value as any).alg)) { + algValues.push(...(value as any).alg); + } + + // Check for the special case 'sd-jwt_alg_values' + if (Array.isArray((value as any)['sd-jwt_alg_values'])) { + algValues.push(...(value as any)['sd-jwt_alg_values']); + } + } + } + + return algValues; +} + function getFormatIntersection(rpFormat: Format, opFormat: Format): Format { - const intersectionFormat: Format = {} + const intersectionFormat: Record = {} const supportedCredentials = getIntersection(Object.keys(rpFormat), Object.keys(opFormat)) if (!supportedCredentials.length) { throw new Error(SIOPErrors.CREDENTIAL_FORMATS_NOT_SUPPORTED) } supportedCredentials.forEach(function (crFormat: string) { - const rpAlgs = [] - const opAlgs = [] - Object.keys(rpFormat[crFormat]).forEach((k) => rpAlgs.push(...rpFormat[crFormat][k])) - Object.keys(opFormat[crFormat]).forEach((k) => opAlgs.push(...opFormat[crFormat][k])) - let methodKeyRP = undefined - let methodKeyOP = undefined - Object.keys(rpFormat[crFormat]).forEach((k) => (methodKeyRP = k)) - Object.keys(opFormat[crFormat]).forEach((k) => (methodKeyOP = k)) + const rpFormatElement = rpFormat[crFormat as keyof Format]; + const opFormatElement = opFormat[crFormat as keyof Format]; + const rpAlgs = collectAlgValues(rpFormatElement); + const opAlgs = collectAlgValues(opFormatElement); + let methodKeyRP = undefined; + let methodKeyOP = undefined; + if (rpFormatElement !== undefined) { + Object.keys(rpFormatElement).forEach((k) => (methodKeyRP = k)); + } + if (opFormatElement !== undefined) { + Object.keys(opFormatElement).forEach((k) => (methodKeyOP = k)); + } if (methodKeyRP !== methodKeyOP) { throw new Error(SIOPErrors.CREDENTIAL_FORMATS_NOT_SUPPORTED) } @@ -107,7 +130,9 @@ function getFormatIntersection(rpFormat: Format, opFormat: Format): Format { throw new Error(SIOPErrors.CREDENTIAL_FORMATS_NOT_SUPPORTED) } intersectionFormat[crFormat] = {} - intersectionFormat[crFormat][methodKeyOP] = algs + if(methodKeyOP !== undefined) { + intersectionFormat[crFormat][methodKeyOP] = algs + } }) return intersectionFormat } diff --git a/packages/siop-oid4vp/lib/helpers/ObjectUtils.ts b/packages/siop-oid4vp/lib/helpers/ObjectUtils.ts index e278902d..1e4e803b 100644 --- a/packages/siop-oid4vp/lib/helpers/ObjectUtils.ts +++ b/packages/siop-oid4vp/lib/helpers/ObjectUtils.ts @@ -9,7 +9,7 @@ export function isStringNullOrEmpty(key: string) { return !key || !key.length } -export function removeNullUndefined(data: unknown) { +export function removeNullUndefined(data: T) : T { if (!data) { return data } @@ -22,5 +22,5 @@ export function removeNullUndefined(data: unknown) { return [key, value] }) //transform the key-value pairs back to an object. - return Object.fromEntries(clean) + return Object.fromEntries(clean) as T } diff --git a/packages/siop-oid4vp/lib/types/Events.ts b/packages/siop-oid4vp/lib/types/Events.ts index ec96e177..e3afa08b 100644 --- a/packages/siop-oid4vp/lib/types/Events.ts +++ b/packages/siop-oid4vp/lib/types/Events.ts @@ -38,7 +38,7 @@ export class AuthorizationEvent { this._error = args.error } - get subject(): T { + get subject(): T | undefined { return this._subject } @@ -46,7 +46,7 @@ export class AuthorizationEvent { return this._timestamp } - get error(): Error { + get error(): Error | undefined { return this._error } diff --git a/packages/siop-oid4vp/lib/types/SIOP.types.ts b/packages/siop-oid4vp/lib/types/SIOP.types.ts index 9d1fe892..40af582d 100644 --- a/packages/siop-oid4vp/lib/types/SIOP.types.ts +++ b/packages/siop-oid4vp/lib/types/SIOP.types.ts @@ -208,9 +208,9 @@ export interface RequestStateInfo { client_id: string // RP ID // sub: string - nonce: string - state: string - iat: number + nonce?: string + state?: string + iat?: number } interface DiscoveryMetadataCommonOpts { diff --git a/packages/siop-oid4vp/lib/types/VpJwtVerifier.ts b/packages/siop-oid4vp/lib/types/VpJwtVerifier.ts index 4982094b..c3bc90bb 100644 --- a/packages/siop-oid4vp/lib/types/VpJwtVerifier.ts +++ b/packages/siop-oid4vp/lib/types/VpJwtVerifier.ts @@ -121,7 +121,9 @@ export const getRequestObjectJwtVerifier = async ( !attestationPayload.exp || typeof attestationPayload.exp !== 'number' || typeof attestationPayload.cnf !== 'object' || - typeof attestationPayload.cnf['jwk'] !== 'object' + !attestationPayload.cnf || + (!('jwk' in attestationPayload.cnf) + || typeof attestationPayload.cnf['jwk'] !== 'object') ) { throw new Error(SIOPErrors.BAD_VERIFIER_ATTESTATION) } @@ -144,7 +146,7 @@ export const getRequestObjectJwtVerifier = async ( } // 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: jwk.alg ?? attestationHeader.alg } + return { method: 'jwk', type, jwk: attestationPayload.cnf['jwk'] as JWK, alg } } else if (clientIdScheme === 'entity_id') { if (!clientId.startsWith('http')) { throw new Error(SIOPErrors.INVALID_REQUEST_OBJECT_ENTITY_ID_SCHEME_CLIENT_ID)