Skip to content

Commit

Permalink
feat: jarm alpha
Browse files Browse the repository at this point in the history
  • Loading branch information
auer-martin committed Sep 27, 2024
1 parent 688fb6d commit cc55d5e
Show file tree
Hide file tree
Showing 13 changed files with 146 additions and 85 deletions.
2 changes: 0 additions & 2 deletions packages/siop-oid4vp/lib/authorization-response/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { IPresentationDefinition, PresentationSignCallBackParams } from '@sphere
import { Format } from '@sphereon/pex-models'
import { CompactSdJwtVc, Hasher, PresentationSubmission, W3CVerifiablePresentation } from '@sphereon/ssi-types'

import { EncryptJwtCallback } from '../helpers/Jwe'
import {
ResponseMode,
ResponseRegistrationOpts,
Expand All @@ -26,7 +25,6 @@ export interface AuthorizationResponseOpts {
version?: SupportedVersion
audience?: string
createJwtCallback: CreateJwtCallback
encryptJwtCallback?: EncryptJwtCallback
jwtIssuer?: JwtIssuer
responseMode?: ResponseMode
responseType?: [ResponseType]
Expand Down
4 changes: 0 additions & 4 deletions packages/siop-oid4vp/lib/helpers/Jwe.ts

This file was deleted.

52 changes: 52 additions & 0 deletions packages/siop-oid4vp/lib/helpers/extract-jwks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { JWK } from '../types'

export type Jwks = {
keys: JWK[]
}

export type JwksMetadataParams = {
jwks?: Jwks
jwks_uri?: string
}

/**
* Fetches a JSON Web Key Set (JWKS) from the specified URI.
*
* @param jwksUri - The URI of the JWKS endpoint.
* @returns A Promise that resolves to the JWKS object.
* @throws Will throw an error if the fetch fails or if the response is not valid JSON.
*/
export async function joseJwksFetch(jwksUri: string): Promise<Jwks | undefined> {
const response = await fetch(jwksUri, {
method: 'GET',
headers: {
Accept: 'application/json',
},
})

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}

const jwks = await response.json()
return jwks
}

/**
* Extracts JSON Web Key Set (JWKS) from the provided metadata.
* If a jwks field is provided, the JWKS will be extracted from the field.
* If a jwks_uri is provided, the JWKS will be fetched from the URI.
*
* @param input - The metadata input to be validated and parsed.
* @returns A promise that resolves to the extracted JWKS or undefined.
* @throws {JoseJwksExtractionError} If the metadata format is invalid or no decryption key is found.
*/
export const joseJwksExtract = async (metadata: JwksMetadataParams) => {
let jwks: Jwks | undefined = metadata.jwks?.keys[0] ? metadata.jwks : undefined

if (!jwks && metadata.jwks_uri) {
jwks = await joseJwksFetch(metadata.jwks_uri)
}

return jwks
}
73 changes: 41 additions & 32 deletions packages/siop-oid4vp/lib/op/OP.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { EventEmitter } from 'events'

import { joseExtractJWKS } from '@protokoll/core'
import { JarmClientMetadataParams, sendJarmAuthRequest } from '@protokoll/jarm'
import { jarmAuthResponseSend } from '@protokoll/jarm'
import { JwtIssuer, uuidv4 } from '@sphereon/oid4vc-common'
import { IIssuerId } from '@sphereon/ssi-types'

Expand All @@ -15,13 +14,15 @@ import {
} from '../authorization-response'
import { encodeJsonAsURI, post } from '../helpers'
import { authorizationRequestVersionDiscovery } from '../helpers/SIOPSpecVersion'
import { joseJwksExtract, JwksMetadataParams } from '../helpers/extract-jwks'
import {
AuthorizationEvent,
AuthorizationEvents,
AuthorizationResponsePayload,
ContentType,
JWK,
ParsedAuthorizationRequestURI,
RegisterEventListener,
RequestObjectPayload,
ResponseIss,
ResponseMode,
SIOPErrors,
Expand Down Expand Up @@ -128,7 +129,7 @@ export class OP {
try {
// IF using DIRECT_POST, the response_uri takes precedence over the redirect_uri
let responseUri = verifiedAuthorizationRequest.responseURI
if (verifiedAuthorizationRequest.authorizationRequestPayload.response_mode === ResponseMode.DIRECT_POST) {
if (verifiedAuthorizationRequest.payload?.response_mode === ResponseMode.DIRECT_POST) {
responseUri = verifiedAuthorizationRequest.authorizationRequestPayload.response_uri ?? responseUri
}

Expand Down Expand Up @@ -157,13 +158,26 @@ export class OP {
}
}

public static async extractEncJwksFromClientMetadata(clientMetadata: JwksMetadataParams) {
// The client metadata will be parsed in the joseExtractJWKS function
const jwks = await joseJwksExtract(clientMetadata)
const encryptionJwk = jwks?.keys.find((key) => key.use === 'enc')
if (!encryptionJwk) {
throw new Error('No encryption jwk could be extracted from the client metadata.')
}

return encryptionJwk
}

// TODO SK Can you please put some documentation on it?
public async submitAuthorizationResponse(
authorizationResponse: AuthorizationResponseWithCorrelationId,
clientMetadata?: {
jwks?: { keys: JWK[] }
jwks_uri?: string
} & JarmClientMetadataParams,
createJarmResponse?: (opts: {
authorizationResponsePayload: AuthorizationResponsePayload
requestObjectPayload: RequestObjectPayload
}) => Promise<{
response: string
}>,
): Promise<Response> {
const { correlationId, response } = authorizationResponse
if (!correlationId) {
Expand All @@ -174,7 +188,8 @@ export class OP {
return responseMode === ResponseMode.DIRECT_POST_JWT || responseMode === ResponseMode.QUERY_JWT || responseMode === ResponseMode.FRAGMENT_JWT
}

const responseMode = response.options.responseMode
const requestObjectPayload = await response.authorizationRequest.requestObject.getPayload()
const { response_mode: responseMode } = requestObjectPayload

if (
!response ||
Expand All @@ -197,44 +212,38 @@ export class OP {
}

if (isJarmResponseMode(responseMode)) {
if (!clientMetadata) {
throw new Error(`Sending an authorization response with response_mode '${responseMode}' requires providing client_metadata`)
}

if (!this._createResponseOptions.encryptJwtCallback) {
throw new Error(`Sending an authorization response with response_mode '${responseMode}' requires providing an encryptJwtCallback`)
}

// The client metadata will be parsed in the joseExtractJWKS function
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const jwks = await joseExtractJWKS(clientMetadata as any)
const dectyptionJwk = jwks.keys.find((key) => key.use === 'enc')
if (!dectyptionJwk) {
throw new Error('No decryption could be extracted from the client metadata')
}

const { jwe } = await this.createResponseOptions.encryptJwtCallback({
jwk: dectyptionJwk,
plaintext: JSON.stringify(response.payload),
})

let responseType: 'id_token' | 'id_token vp_token' | 'vp_token'
if (idToken && payload.vp_token) {
responseType = 'id_token vp_token'
} else if (idToken) {
responseType = 'id_token'
} else if (payload.vp_token) {
responseType = 'vp_token'
} else {
throw new Error('No id_token or vp_token present in the response payload')
}

return sendJarmAuthRequest({
const { response } = await createJarmResponse({
requestObjectPayload,
authorizationResponsePayload: payload,
})

return jarmAuthResponseSend({
authRequestParams: {
response_uri: responseUri,
response_mode: responseMode,
response_type: responseType,
},
authResponseParams: { response: jwe },
authResponse: response,
})
.then((result) => {
void this.emitEvent(AuthorizationEvents.ON_AUTH_RESPONSE_SENT_SUCCESS, { correlationId, subject: response })
return result
})
.catch((error: Error) => {
void this.emitEvent(AuthorizationEvents.ON_AUTH_RESPONSE_SENT_FAILED, { correlationId, subject: response, error })
throw error
})
}

const authResponseAsURI = encodeJsonAsURI(payload, { arraysWithIndex: ['presentation_submission'] })
Expand Down
7 changes: 0 additions & 7 deletions packages/siop-oid4vp/lib/op/OPBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Hasher, IIssuerId } from '@sphereon/ssi-types'

import { PropertyTargets } from '../authorization-request'
import { PresentationSignCallback } from '../authorization-response'
import { EncryptJwtCallback } from '../helpers/Jwe'
import { ResponseIss, ResponseMode, ResponseRegistrationOpts, SupportedVersion, VerifyJwtCallback } from '../types'
import { CreateJwtCallback } from '../types/VpJwtIssuer'

Expand All @@ -16,7 +15,6 @@ export class OPBuilder {
responseMode?: ResponseMode = ResponseMode.DIRECT_POST
responseRegistration?: Partial<ResponseRegistrationOpts> = {}
createJwtCallback?: CreateJwtCallback
encryptJwtCallback?: EncryptJwtCallback
verifyJwtCallback?: VerifyJwtCallback
presentationSignCallback?: PresentationSignCallback
supportedVersions?: SupportedVersion[]
Expand Down Expand Up @@ -66,11 +64,6 @@ export class OPBuilder {
return this
}

withEncryptJwtCallback(encryptJwtCallback: EncryptJwtCallback): OPBuilder {
this.encryptJwtCallback = encryptJwtCallback
return this
}

withVerifyJwtCallback(verifyJwtCallback: VerifyJwtCallback): OPBuilder {
this.verifyJwtCallback = verifyJwtCallback
return this
Expand Down
1 change: 0 additions & 1 deletion packages/siop-oid4vp/lib/op/Opts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export const createResponseOptsFromBuilderOrExistingOpts = (opts: {
expiresIn: opts.builder.expiresIn,
jwtIssuer: responseOpts?.jwtIssuer,
createJwtCallback: opts.builder.createJwtCallback,
encryptJwtCallback: opts.builder.encryptJwtCallback,
responseMode: opts.builder.responseMode,
...(responseOpts?.version
? { version: responseOpts.version }
Expand Down
1 change: 1 addition & 0 deletions packages/siop-oid4vp/lib/request-object/Payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const createRequestObjectPayload = async (opts: CreateAuthorizationReques
claims,
presentation_definition_uri: payload.presentation_definition_uri,
presentation_definition: payload.presentation_definition,
client_metadata: payload.client_metadata,
iat,
nbf,
exp,
Expand Down
35 changes: 34 additions & 1 deletion packages/siop-oid4vp/lib/rp/RP.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { EventEmitter } from 'events'

import {
jarmAuthResponseDirectPostJwtValidate,
JarmAuthResponseParams,
JarmDirectPostJwtAuthResponseValidationContext,
JarmDirectPostJwtResponseParams,
} from '@protokoll/jarm'
import { JwtIssuer, uuidv4 } from '@sphereon/oid4vc-common'
import { Hasher } from '@sphereon/ssi-types'

Expand All @@ -15,12 +21,13 @@ import {
import { mergeVerificationOpts } from '../authorization-request/Opts'
import { AuthorizationResponse, PresentationDefinitionWithLocation, VerifyAuthorizationResponseOpts } from '../authorization-response'
import { getNonce, getState } from '../helpers'
import { PassBy } from '../types'
import {
AuthorizationEvent,
AuthorizationEvents,
AuthorizationResponsePayload,
PassBy,
RegisterEventListener,
RequestObjectPayload,
ResponseURIType,
SIOPErrors,
SupportedVersion,
Expand Down Expand Up @@ -133,6 +140,32 @@ export class RP {
})
}

static async processJarmAuthorizationResponse(
response: string,
opts: {
decryptCompact: (input: {
jwk: { kid: string }
jwe: string
}) => Promise<{ plaintext: string; protectedHeader: Record<string, unknown> & { alg: string; enc: string } }>
getAuthRequestPayload: (input: JarmDirectPostJwtResponseParams | JarmAuthResponseParams) => Promise<{ authRequestParams: RequestObjectPayload }>
},
) {
const { decryptCompact, getAuthRequestPayload } = opts

const getParams = getAuthRequestPayload as JarmDirectPostJwtAuthResponseValidationContext['openid4vp']['authRequest']['getParams']

const validatedResponse = await jarmAuthResponseDirectPostJwtValidate(
{ response },
{
openid4vp: { authRequest: { getParams } },
// @ts-expect-error for now we don't support signing
jose: { jwe: { decryptCompact } },
},
)

return validatedResponse
}

public async verifyAuthorizationResponse(
authorizationResponsePayload: AuthorizationResponsePayload,
opts?: {
Expand Down
7 changes: 0 additions & 7 deletions packages/siop-oid4vp/lib/rp/RPBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Hasher } from '@sphereon/ssi-types'

import { PropertyTarget, PropertyTargets } from '../authorization-request'
import { PresentationVerificationCallback } from '../authorization-response'
import { DecryptJwtCallback } from '../helpers/Jwe'
import { ClientIdScheme, CreateJwtCallback, RequestAud, VerifyJwtCallback } from '../types'
import {
AuthorizationRequestPayload,
Expand All @@ -28,7 +27,6 @@ import { IRPSessionManager } from './types'
export class RPBuilder {
requestObjectBy: ObjectBy
createJwtCallback?: CreateJwtCallback
decryptJwtCallback: DecryptJwtCallback
verifyJwtCallback?: VerifyJwtCallback
revocationVerification?: RevocationVerification
revocationVerificationCallback?: RevocationVerificationCallback
Expand Down Expand Up @@ -214,11 +212,6 @@ export class RPBuilder {
return this
}

withDecryptJwtCallback(decryptJwtCallback: DecryptJwtCallback): RPBuilder {
this.decryptJwtCallback = decryptJwtCallback
return this
}

withPresentationDefinition(definitionOpts: { definition: IPresentationDefinition; definitionUri?: string }, targets?: PropertyTargets): RPBuilder {
const { definition, definitionUri } = definitionOpts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ export const AuthorizationResponseOptsSchemaObj = {
"createJwtCallback": {
"$ref": "#/definitions/CreateJwtCallback"
},
"encryptJwtCallback": {
"$ref": "#/definitions/EncryptJwtCallback"
},
"jwtIssuer": {
"$ref": "#/definitions/JwtIssuer"
},
Expand Down Expand Up @@ -1556,14 +1553,6 @@ export const AuthorizationResponseOptsSchemaObj = {
}
}
},
"EncryptJwtCallback": {
"properties": {
"isFunction": {
"type": "boolean",
"const": true
}
}
},
"JwtIssuer": {
"anyOf": [
{
Expand Down
4 changes: 3 additions & 1 deletion packages/siop-oid4vp/lib/types/SIOP.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
PresentationVerificationCallback,
VerifyAuthorizationResponseOpts,
} from '../authorization-response'
import { JwksMetadataParams } from '../helpers/extract-jwks'
import { RequestObject, RequestObjectOpts } from '../request-object'
import { IRPSessionManager } from '../rp'

Expand All @@ -34,6 +35,7 @@ export interface RequestObjectPayload extends RequestCommonPayload, JWTPayload {
response_type: ResponseType | string // REQUIRED. Constant string value id_token.
client_id: string // REQUIRED. RP's identifier at the Self-Issued OP.
client_id_scheme?: ClientIdScheme // The client_id_scheme enables deployments of this specification to use different mechanisms to obtain and validate metadata of the Verifier beyond the scope of [RFC6749]. The term client_id_scheme is used since the Verifier is acting as an OAuth 2.0 Client.
client_metadata: ClientMetadataOpts
redirect_uri?: string // REQUIRED before OID4VP v18, now optional because of response_uri. URI to which the Self-Issued OP Response will be sent
response_uri?: string // New since OID4VP18 OPTIONAL. The Response URI to which the Wallet MUST send the Authorization Response using an HTTPS POST request as defined by the Response Mode direct_post. The Response URI receives all Authorization Response parameters as defined by the respective Response Type. 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.
nonce: string
Expand Down Expand Up @@ -374,7 +376,7 @@ export type DiscoveryMetadataPayload = DiscoveryMetadataPayloadVID1 | JWT_VCDisc
export type DiscoveryMetadataOpts = (JWT_VCDiscoveryMetadataOpts | DiscoveryMetadataOptsVID1 | DiscoveryMetadataOptsVD11) &
DiscoveryMetadataCommonOpts

export type ClientMetadataOpts = RPRegistrationMetadataOpts & ClientMetadataProperties & JarmClientMetadataParams
export type ClientMetadataOpts = RPRegistrationMetadataOpts & ClientMetadataProperties & JarmClientMetadataParams & JwksMetadataParams

export type ResponseRegistrationOpts = DiscoveryMetadataOpts & ClientMetadataProperties

Expand Down
Loading

0 comments on commit cc55d5e

Please sign in to comment.