Skip to content

Commit

Permalink
fix: oid4vp can be used separate from idtoken (#1827)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <timo@animo.id>
  • Loading branch information
TimoGlastra committed Apr 25, 2024
1 parent e3b10ee commit ca383c2
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 103 deletions.
6 changes: 3 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
"@multiformats/base-x": "^4.0.1",
"@sd-jwt/core": "^0.2.1",
"@sd-jwt/decode": "^0.2.1",
"@sphereon/pex": "3.3.0",
"@sphereon/pex-models": "^2.2.2",
"@sphereon/ssi-types": "^0.18.1",
"@sphereon/pex": "^3.3.2",
"@sphereon/pex-models": "^2.2.4",
"@sphereon/ssi-types": "^0.23.0",
"@stablelib/ed25519": "^1.0.2",
"@stablelib/sha256": "^1.0.1",
"@types/ws": "^8.5.4",
Expand Down
4 changes: 2 additions & 2 deletions packages/openid4vc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
},
"dependencies": {
"@credo-ts/core": "0.5.1",
"@sphereon/did-auth-siop": "0.6.2",
"@sphereon/did-auth-siop": "^0.6.4",
"@sphereon/oid4vci-client": "^0.10.2",
"@sphereon/oid4vci-common": "^0.10.1",
"@sphereon/oid4vci-issuer": "^0.10.2",
"@sphereon/ssi-types": "^0.18.1",
"@sphereon/ssi-types": "^0.23.0",
"class-transformer": "^0.5.1",
"rxjs": "^7.8.0"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,17 @@ export class OpenId4VcSiopVerifierService {
requestByReferenceURI: hostedAuthorizationRequestUri,
})

const authorizationRequestUri = await authorizationRequest.uri()
// NOTE: it's not possible to set the uri scheme when using the RP to create an auth request, only lower level
// functions allow this. So we need to replace the uri scheme manually.
let authorizationRequestUri = (await authorizationRequest.uri()).encodedUri
if (options.presentationExchange && !options.idToken) {
authorizationRequestUri = authorizationRequestUri.replace('openid://', 'openid4vp://')
}

const verificationSession = await verificationSessionCreatedPromise

return {
authorizationRequest: authorizationRequestUri.encodedUri,
authorizationRequest: authorizationRequestUri,
verificationSession,
}
}
Expand Down Expand Up @@ -193,7 +199,8 @@ export class OpenId4VcSiopVerifierService {
(e) =>
e.payload.record.id === options.verificationSession.id &&
e.payload.record.verifierId === options.verificationSession.verifierId &&
e.payload.record.state === OpenId4VcVerificationSessionState.ResponseVerified
(e.payload.record.state === OpenId4VcVerificationSessionState.ResponseVerified ||
e.payload.record.state === OpenId4VcVerificationSessionState.Error)
),
first(),
timeout({
Expand Down Expand Up @@ -353,10 +360,12 @@ export class OpenId4VcSiopVerifierService {
agentContext: AgentContext,
verifierId: string,
{
idToken,
presentationDefinition,
requestSigner,
clientId,
}: {
idToken?: boolean
presentationDefinition?: DifPresentationExchangeDefinition
requestSigner?: OpenId4VcJwtIssuer
clientId?: string
Expand Down Expand Up @@ -387,6 +396,17 @@ export class OpenId4VcSiopVerifierService {
throw new CredoError("Either 'requestSigner' or 'clientId' must be provided.")
}

const responseTypes: ResponseType[] = []
if (!presentationDefinition && idToken === false) {
throw new CredoError('Either `presentationExchange` or `idToken` must be enabled')
}
if (presentationDefinition) {
responseTypes.push(ResponseType.VP_TOKEN)
}
if (idToken === true || !presentationDefinition) {
responseTypes.push(ResponseType.ID_TOKEN)
}

// FIXME: we now manually remove did:peer, we should probably allow the user to configure this
const supportedDidMethods = agentContext.dependencyManager
.resolve(DidsApi)
Expand All @@ -402,12 +422,22 @@ export class OpenId4VcSiopVerifierService {
.withRedirectUri(authorizationResponseUrl)
.withIssuer(ResponseIss.SELF_ISSUED_V2)
.withSupportedVersions([SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_D12_OID4VP_D18])
.withCustomResolver(getSphereonDidResolver(agentContext))
.withResponseMode(ResponseMode.POST)
.withHasher(Hasher.hash)
.withCheckLinkedDomain(CheckLinkedDomain.NEVER)
// FIXME: should allow verification of revocation
// .withRevocationVerificationCallback()
.withRevocationVerification(RevocationVerification.NEVER)
.withSessionManager(new OpenId4VcRelyingPartySessionManager(agentContext, verifierId))
.withEventEmitter(sphereonEventEmitter)
.withResponseType(responseTypes)

// TODO: we should probably allow some dynamic values here
.withClientMetadata({
client_id: _clientId,
passBy: PassBy.VALUE,
idTokenSigningAlgValuesSupported: supportedAlgs as SigningAlgo[],
responseTypesSupported: [ResponseType.VP_TOKEN, ResponseType.ID_TOKEN],
responseTypesSupported: [ResponseType.VP_TOKEN],
subject_syntax_types_supported: supportedDidMethods.map((m) => `did:${m}`),
vpFormatsSupported: {
jwt_vc: {
Expand All @@ -431,21 +461,13 @@ export class OpenId4VcSiopVerifierService {
},
},
})
.withCustomResolver(getSphereonDidResolver(agentContext))
.withResponseMode(ResponseMode.POST)
.withResponseType(presentationDefinition ? [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN] : ResponseType.ID_TOKEN)
.withScope('openid')
.withHasher(Hasher.hash)
.withCheckLinkedDomain(CheckLinkedDomain.NEVER)
// FIXME: should allow verification of revocation
// .withRevocationVerificationCallback()
.withRevocationVerification(RevocationVerification.NEVER)
.withSessionManager(new OpenId4VcRelyingPartySessionManager(agentContext, verifierId))
.withEventEmitter(sphereonEventEmitter)

if (presentationDefinition) {
builder.withPresentationDefinition({ definition: presentationDefinition }, [PropertyTarget.REQUEST_OBJECT])
}
if (responseTypes.includes(ResponseType.ID_TOKEN)) {
builder.withScope('openid')
}

for (const supportedDidMethod of supportedDidMethods) {
builder.addDidMethod(supportedDidMethod)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export interface OpenId4VcSiopCreateAuthorizationRequestOptions {
*/
requestSigner: OpenId4VcJwtIssuer

/**
* Whether to reuqest an ID Token. Enabled by defualt when `presentationExchange` is not provided,
* disabled by default when `presentationExchange` is provided.
*/
idToken?: boolean

/**
* A DIF Presentation Definition (v2) can be provided to request a Verifiable Presentation using OpenID4VP.
*/
Expand All @@ -39,7 +45,7 @@ export interface OpenId4VcSiopCreateAuthorizationRequestReturn {
}

/**
* Either `idToken` and/or `presentationExchange` will be present, but not none.
* Either `idToken` and/or `presentationExchange` will be present.
*/
export interface OpenId4VcSiopVerifiedAuthorizationResponse {
idToken?: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('OpenId4VcVerifier', () => {
enableNetConnect()
})

it('check openid proof request format', async () => {
it('check openid proof request format (vp token)', async () => {
const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier()
const { authorizationRequest, verificationSession } =
await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({
Expand All @@ -47,6 +47,43 @@ describe('OpenId4VcVerifier', () => {
},
})

expect(
authorizationRequest.startsWith(
`openid4vp://?request_uri=http%3A%2F%2Fredirect-uri%2F${openIdVerifier.verifierId}%2Fauthorization-requests%2F`
)
).toBe(true)

const jwt = Jwt.fromSerializedJwt(verificationSession.authorizationRequestJwt)

expect(jwt.header.kid)

expect(jwt.header.kid).toEqual(verifier.kid)
expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA)
expect(jwt.header.typ).toEqual('JWT')
expect(jwt.payload.additionalClaims.scope).toEqual('openid')
expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.did)
expect(jwt.payload.additionalClaims.redirect_uri).toEqual(
`http://redirect-uri/${openIdVerifier.verifierId}/authorize`
)
expect(jwt.payload.additionalClaims.response_mode).toEqual('post')
expect(jwt.payload.additionalClaims.nonce).toBeDefined()
expect(jwt.payload.additionalClaims.state).toBeDefined()
expect(jwt.payload.additionalClaims.response_type).toEqual('vp_token')
expect(jwt.payload.iss).toEqual(verifier.did)
expect(jwt.payload.sub).toEqual(verifier.did)
})

it('check openid proof request format (id token)', async () => {
const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier()
const { authorizationRequest, verificationSession } =
await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({
requestSigner: {
method: 'did',
didUrl: verifier.kid,
},
verifierId: openIdVerifier.verifierId,
})

expect(
authorizationRequest.startsWith(
`openid://?request_uri=http%3A%2F%2Fredirect-uri%2F${openIdVerifier.verifierId}%2Fauthorization-requests%2F`
Expand All @@ -68,7 +105,7 @@ describe('OpenId4VcVerifier', () => {
expect(jwt.payload.additionalClaims.response_mode).toEqual('post')
expect(jwt.payload.additionalClaims.nonce).toBeDefined()
expect(jwt.payload.additionalClaims.state).toBeDefined()
expect(jwt.payload.additionalClaims.response_type).toEqual('id_token vp_token')
expect(jwt.payload.additionalClaims.response_type).toEqual('id_token')
expect(jwt.payload.iss).toEqual(verifier.did)
expect(jwt.payload.sub).toEqual(verifier.did)
})
Expand Down
103 changes: 75 additions & 28 deletions packages/openid4vc/tests/openid4vc.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,75 @@ describe('OpenId4Vc', () => {
await holderTenant1.endSession()
})

it('e2e flow with tenants only requesting an id-token', async () => {
const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId })
const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId })

const openIdVerifierTenant1 = await verifierTenant1.modules.openId4VcVerifier.createVerifier()

const { authorizationRequest: authorizationRequestUri1, verificationSession: verificationSession } =
await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({
verifierId: openIdVerifierTenant1.verifierId,
requestSigner: {
method: 'did',
didUrl: verifier1.verificationMethod.id,
},
})

expect(authorizationRequestUri1).toEqual(
`openid://?request_uri=${encodeURIComponent(verificationSession.authorizationRequestUri)}`
)

await verifierTenant1.endSession()

const resolvedAuthorizationRequest = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest(
authorizationRequestUri1
)

expect(resolvedAuthorizationRequest.presentationExchange).toBeUndefined()

const { submittedResponse: submittedResponse1, serverResponse: serverResponse1 } =
await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({
authorizationRequest: resolvedAuthorizationRequest.authorizationRequest,
openIdTokenIssuer: {
method: 'did',
didUrl: holder1.verificationMethod.id,
},
})

expect(submittedResponse1).toEqual({
expires_in: 6000,
id_token: expect.any(String),
state: expect.any(String),
})
expect(serverResponse1).toMatchObject({
status: 200,
})

// The RP MUST validate that the aud (audience) Claim contains the value of the client_id
// that the RP sent in the Authorization Request as an audience.
// When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier.
const verifierTenant1_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId })
await waitForVerificationSessionRecordSubject(verifier.replaySubject, {
contextCorrelationId: verifierTenant1_2.context.contextCorrelationId,
state: OpenId4VcVerificationSessionState.ResponseVerified,
verificationSessionId: verificationSession.id,
})

const { idToken, presentationExchange } =
await verifierTenant1_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession.id)

const requestObjectPayload = JsonEncoder.fromBase64(
verificationSession.authorizationRequestJwt?.split('.')[1] as string
)
expect(idToken?.payload).toMatchObject({
state: requestObjectPayload.state,
nonce: requestObjectPayload.nonce,
})

expect(presentationExchange).toBeUndefined()
})

it('e2e flow with tenants, verifier endpoints verifying a jwt-vc', async () => {
const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId })
const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId })
Expand Down Expand Up @@ -384,7 +453,7 @@ describe('OpenId4Vc', () => {
})

expect(authorizationRequestUri1).toEqual(
`openid://?request_uri=${encodeURIComponent(verificationSession1.authorizationRequestUri)}`
`openid4vp://?request_uri=${encodeURIComponent(verificationSession1.authorizationRequestUri)}`
)

const { authorizationRequest: authorizationRequestUri2, verificationSession: verificationSession2 } =
Expand All @@ -400,7 +469,7 @@ describe('OpenId4Vc', () => {
})

expect(authorizationRequestUri2).toEqual(
`openid://?request_uri=${encodeURIComponent(verificationSession2.authorizationRequestUri)}`
`openid4vp://?request_uri=${encodeURIComponent(verificationSession2.authorizationRequestUri)}`
)

await verifierTenant1.endSession()
Expand Down Expand Up @@ -477,7 +546,6 @@ describe('OpenId4Vc', () => {

expect(submittedResponse1).toEqual({
expires_in: 6000,
id_token: expect.any(String),
presentation_submission: {
definition_id: 'OpenBadgeCredential',
descriptor_map: [
Expand Down Expand Up @@ -514,14 +582,7 @@ describe('OpenId4Vc', () => {
const { idToken: idToken1, presentationExchange: presentationExchange1 } =
await verifierTenant1_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession1.id)

const requestObjectPayload1 = JsonEncoder.fromBase64(
verificationSession1.authorizationRequestJwt?.split('.')[1] as string
)
expect(idToken1?.payload).toMatchObject({
state: requestObjectPayload1.state,
nonce: requestObjectPayload1.nonce,
})

expect(idToken1).toBeUndefined()
expect(presentationExchange1).toMatchObject({
definition: openBadgePresentationDefinition,
submission: {
Expand Down Expand Up @@ -564,14 +625,7 @@ describe('OpenId4Vc', () => {
})
const { idToken: idToken2, presentationExchange: presentationExchange2 } =
await verifierTenant2_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession2.id)

const requestObjectPayload2 = JsonEncoder.fromBase64(
verificationSession2.authorizationRequestJwt?.split('.')[1] as string
)
expect(idToken2?.payload).toMatchObject({
state: requestObjectPayload2.state,
nonce: requestObjectPayload2.nonce,
})
expect(idToken2).toBeUndefined()

expect(presentationExchange2).toMatchObject({
definition: universityDegreePresentationDefinition,
Expand Down Expand Up @@ -658,7 +712,7 @@ describe('OpenId4Vc', () => {
})

expect(authorizationRequest).toEqual(
`openid://?request_uri=${encodeURIComponent(verificationSession.authorizationRequestUri)}`
`openid4vp://?request_uri=${encodeURIComponent(verificationSession.authorizationRequestUri)}`
)

const resolvedAuthorizationRequest = await holder.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest(
Expand Down Expand Up @@ -726,7 +780,6 @@ describe('OpenId4Vc', () => {
expect(submittedResponse.presentation_submission?.descriptor_map[0].path_nested).toBeUndefined()
expect(submittedResponse).toEqual({
expires_in: 6000,
id_token: expect.any(String),
presentation_submission: {
definition_id: 'OpenBadgeCredential',
descriptor_map: [
Expand Down Expand Up @@ -756,13 +809,7 @@ describe('OpenId4Vc', () => {
const { idToken, presentationExchange } =
await verifier.agent.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession.id)

const requestObjectPayload = JsonEncoder.fromBase64(
verificationSession.authorizationRequestJwt?.split('.')[1] as string
)
expect(idToken?.payload).toMatchObject({
state: requestObjectPayload.state,
nonce: requestObjectPayload.nonce,
})
expect(idToken).toBeUndefined()

const presentation = presentationExchange?.presentations[0] as SdJwtVc

Expand Down
Loading

0 comments on commit ca383c2

Please sign in to comment.