Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: oid4vp can be used separate from idtoken #1827

Merged
merged 7 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading