Skip to content

Commit be377c9

Browse files
committed
chore: authorization_details tests passing
1 parent c7d6fcd commit be377c9

5 files changed

Lines changed: 137 additions & 16 deletions

File tree

packages/issuer-rest/lib/IssuerTokenEndpoint.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { DPoPVerifyJwtCallback, JWK, uuidv4, verifyDPoP } from '@sphereon/oid4vc-common'
2-
import { GrantTypes, PRE_AUTHORIZED_CODE_REQUIRED_ERROR, TokenError, TokenErrorResponse } from '@sphereon/oid4vci-common'
2+
import {
3+
AuthorizationRequest,
4+
GrantTypes,
5+
PRE_AUTHORIZED_CODE_REQUIRED_ERROR,
6+
TokenError,
7+
TokenErrorResponse
8+
} from '@sphereon/oid4vci-common'
39
import { assertValidAccessTokenRequest, createAccessTokenResponse, ITokenEndpointOpts, VcIssuer } from '@sphereon/oid4vci-issuer'
410
import { sendErrorResponse } from '@sphereon/ssi-express-support'
511
import { NextFunction, Request, Response } from 'express'
@@ -29,7 +35,7 @@ export const handleTokenRequest = ({
2935
dPoPVerifyJwtCallback: DPoPVerifyJwtCallback
3036
}
3137
// The full URL of the access token endpoint
32-
accessTokenEndpoint?: string
38+
accessTokenEndpoint?: string,
3339
}) => {
3440
return async (request: Request, response: Response) => {
3541
response.set({
@@ -115,14 +121,19 @@ export const handleTokenRequest = ({
115121
}
116122

117123
export const verifyTokenRequest = ({
118-
preAuthorizedCodeExpirationDuration,
119-
issuer,
120-
}: Required<Pick<ITokenEndpointOpts, 'preAuthorizedCodeExpirationDuration'> & { issuer: VcIssuer }>) => {
124+
preAuthorizedCodeExpirationDuration,
125+
issuer,
126+
authRequestsData
127+
}: Required<Pick<ITokenEndpointOpts, 'preAuthorizedCodeExpirationDuration'>> & {
128+
issuer: VcIssuer,
129+
authRequestsData?: Map<string, AuthorizationRequest>
130+
}) => {
121131
return async (request: Request, response: Response, next: NextFunction) => {
122132
try {
123133
await assertValidAccessTokenRequest(request.body, {
124134
expirationDuration: preAuthorizedCodeExpirationDuration,
125135
credentialOfferSessions: issuer.credentialOfferSessions,
136+
authRequestsData
126137
})
127138
} catch (error) {
128139
if (error instanceof TokenError) {

packages/issuer-rest/lib/OID4VCIServer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,11 @@ export class OID4VCIServer {
204204
})
205205
this.assertAccessTokenHandling()
206206
if (!this.isTokenEndpointDisabled(opts?.endpointOpts?.tokenEndpointOpts, opts?.asClientOpts)) {
207-
accessTokenEndpoint(this.router, this.issuer, { ...opts?.endpointOpts?.tokenEndpointOpts, baseUrl: this.baseUrl })
207+
accessTokenEndpoint(this.router, this.issuer, {
208+
...opts?.endpointOpts?.tokenEndpointOpts,
209+
baseUrl: this.baseUrl,
210+
authRequestsData: this.authRequestsData
211+
})
208212
}
209213
if (this.isStatusEndpointEnabled(opts?.endpointOpts?.getStatusOpts)) {
210214
getIssueStatusEndpoint(this.router, this.issuer, { ...opts?.endpointOpts?.getStatusOpts, baseUrl: this.baseUrl })

packages/issuer-rest/lib/oid4vci-api-functions.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,8 @@ export function authorizationChallengeEndpoint(
220220
}
221221

222222
export function accessTokenEndpoint(router: Router, issuer: VcIssuer, opts: ITokenEndpointOpts & ISingleEndpointOpts & {
223-
baseUrl: string | URL
223+
baseUrl: string | URL,
224+
authRequestsData?: Map<string, AuthorizationRequest>
224225
}) {
225226
const externalAS = isExternalAS(issuer.issuerMetadata) || issuer.asClientOpts
226227
if (externalAS || (opts.accessTokenProvider && opts.accessTokenProvider !== 'internal')) {
@@ -263,12 +264,14 @@ export function accessTokenEndpoint(router: Router, issuer: VcIssuer, opts: ITok
263264

264265
LOG.log(`[OID4VCI] Token endpoint enabled at ${url.toString()}`)
265266

267+
266268
// this.issuer.issuerMetadata.token_endpoint = url.toString()
267269
router.post(
268270
determinePath(baseUrl, url.pathname, { stripBasePath: true }),
269271
verifyTokenRequest({
270272
issuer,
271-
preAuthorizedCodeExpirationDuration
273+
preAuthorizedCodeExpirationDuration,
274+
authRequestsData: opts.authRequestsData
272275
}),
273276
handleTokenRequest({
274277
issuer,

packages/issuer/lib/__tests__/VcIssuer.spec.ts

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ import { VcIssuer } from '../VcIssuer'
88
import { AuthorizationServerMetadataBuilder, CredentialSupportedBuilderV1_15, VcIssuerBuilder } from '../builder'
99
import { MemoryStates } from '../state-manager'
1010
import {
11-
Alg, ALG_ERROR,
11+
Alg,
12+
ALG_ERROR,
1213
AuthorizationDetailsV1_0_15,
1314
CredentialConfigurationSupportedV1_0_15,
1415
CredentialOfferSession,
15-
IssueStatus, STATE_MISSING_ERROR
16+
GrantTypes,
17+
IssueStatus,
18+
STATE_MISSING_ERROR
1619
} from '@sphereon/oid4vci-common'
20+
import { createAccessTokenResponse } from '../tokens'
1721

1822
const IDENTIPROOF_ISSUER_URL = 'https://issuer.research.identiproof.io'
1923

@@ -334,6 +338,105 @@ describe('VcIssuer', () => {
334338
expect(typeof result.credentials[0].credential).toBe('object')
335339
})
336340

341+
it('should generate credential_identifiers in token response and accept them in credential request', async () => {
342+
const createdAt = +new Date()
343+
344+
// Setup session with authorization_details
345+
const authorizationDetails: AuthorizationDetailsV1_0_15[] = [{
346+
type: 'openid_credential',
347+
credential_configuration_id: 'UniversityDegree_JWT',
348+
format: 'jwt_vc_json' as const,
349+
types: ['VerifiableCredential', 'UniversityDegree_JWT']
350+
}]
351+
352+
await vcIssuer.credentialOfferSessions.set('test-pre-authorized-code', {
353+
createdAt: createdAt,
354+
notification_id: '43243',
355+
preAuthorizedCode: 'test-pre-authorized-code',
356+
credentialOffer: {
357+
credential_offer: {
358+
credential_issuer: 'did:key:test',
359+
credential_configuration_ids: ['UniversityDegree_JWT']
360+
}
361+
},
362+
authorizationDetails: authorizationDetails,
363+
lastUpdatedAt: createdAt,
364+
status: IssueStatus.ACCESS_TOKEN_CREATED
365+
})
366+
367+
const tokenResponse = await createAccessTokenResponse({
368+
grant_type: GrantTypes.PRE_AUTHORIZED_CODE,
369+
'pre-authorized_code': 'test-pre-authorized-code'
370+
}, {
371+
credentialOfferSessions: vcIssuer.credentialOfferSessions,
372+
cNonces: vcIssuer.cNonces,
373+
tokenExpiresIn: 300,
374+
accessTokenSignerCallback: async () => 'mock-access-token',
375+
accessTokenIssuer: 'test-issuer'
376+
})
377+
378+
// Verify token response includes authorization_details with generated credential_identifiers
379+
expect(tokenResponse.authorization_details).toBeDefined()
380+
expect(tokenResponse.authorization_details).toHaveLength(1)
381+
expect(tokenResponse.authorization_details![0]).toHaveProperty('credential_identifiers')
382+
expect(tokenResponse.authorization_details![0].credential_identifiers).toHaveLength(1)
383+
384+
const generatedIdentifier = tokenResponse.authorization_details![0].credential_identifiers![0]
385+
expect(generatedIdentifier).toMatch(/UniversityDegree_JWT_/)
386+
387+
// Step 2: Mock JWT verification for credential request
388+
jwtVerifyCallback.mockResolvedValue({
389+
did: 'did:example:1234',
390+
kid: 'did:example:1234#auth',
391+
alg: Alg.ES256K,
392+
didDocument: {
393+
'@context': 'https://www.w3.org/ns/did/v1',
394+
id: 'did:example:1234'
395+
},
396+
jwt: {
397+
header: {
398+
typ: 'openid4vci-proof+jwt',
399+
alg: Alg.ES256K,
400+
kid: 'test-kid'
401+
},
402+
payload: {
403+
aud: IDENTIPROOF_ISSUER_URL,
404+
iat: +new Date() / 1000,
405+
nonce: 'test-nonce',
406+
// Include authorization_details from token response
407+
authorization_details: tokenResponse.authorization_details
408+
}
409+
}
410+
})
411+
412+
await vcIssuer.cNonces.set('test-nonce', {
413+
cNonce: 'test-nonce',
414+
createdAt: createdAt
415+
})
416+
417+
// Step 3: Use generated credential_identifier in credential request
418+
const credentialResult = await vcIssuer.issueCredential({
419+
credential: verifiableCredential,
420+
credentialRequest: {
421+
credential_identifier: generatedIdentifier,
422+
proof: {
423+
proof_type: 'jwt',
424+
jwt: 'ye.ye.ye'
425+
}
426+
} as any,
427+
issuerCorrelation: {
428+
preAuthorizedCode: 'test-pre-authorized-code',
429+
authorizationDetails: tokenResponse.authorization_details
430+
},
431+
newCNonce: 'new-test-nonce'
432+
})
433+
434+
// Verify credential was issued successfully
435+
expect(credentialResult.credentials).toHaveLength(1)
436+
expect(credentialResult.credentials[0].credential).toBeDefined()
437+
expect(credentialResult.notification_id).toBe('43243')
438+
})
439+
337440
it('should reject invalid credential_identifier', async () => {
338441
jwtVerifyCallback.mockResolvedValue({
339442
did: 'did:example:1234',

packages/issuer/lib/tokens/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { calculateJwkThumbprint, JWK, uuidv4 } from '@sphereon/oid4vc-common'
22
import {
33
AccessTokenRequest,
44
AccessTokenResponse,
5-
Alg,
5+
Alg, AuthorizationRequest,
66
CNonceState,
77
CredentialOfferSession,
88
EXPIRED_PRE_AUTHORIZED_CODE,
@@ -22,7 +22,7 @@ import {
2222
UNSUPPORTED_GRANT_TYPE_ERROR,
2323
USER_PIN_NOT_REQUIRED_ERROR,
2424
USER_PIN_REQUIRED_ERROR,
25-
USER_PIN_TX_CODE_SPEC_ERROR,
25+
USER_PIN_TX_CODE_SPEC_ERROR
2626
} from '@sphereon/oid4vci-common'
2727

2828
import { generateCredentialIdentifiers, isPreAuthorizedCodeExpired } from '../functions'
@@ -105,7 +105,7 @@ export const assertValidAccessTokenRequest = async (
105105
opts: {
106106
credentialOfferSessions: IStateManager<CredentialOfferSession>
107107
expirationDuration: number
108-
authRequestsData?: Map<string, any> // Add this for authorization code flow
108+
authRequestsData?: Map<string, any>
109109
}
110110
) => {
111111
const { credentialOfferSessions, expirationDuration, authRequestsData } = opts
@@ -118,7 +118,7 @@ export const assertValidAccessTokenRequest = async (
118118

119119
// Find the authorization request data by code
120120
// This is simplified - you'll need to implement proper code->request mapping
121-
const authRequestData = Array.from(authRequestsData.values())
121+
const authRequestData:AuthorizationRequest | undefined = Array.from(authRequestsData.values())
122122
.find(data => data.authorization_code === request.code)
123123

124124
if (!authRequestData) {
@@ -143,12 +143,12 @@ export const assertValidAccessTokenRequest = async (
143143
grants: {}
144144
}
145145
},
146-
authorizationDetails: authRequestData.authorization_details,
146+
authorizationDetails: authRequestData?.authorization_details,
147147
authorizationCode: request.code
148148
}
149149
await credentialOfferSessions.set(sessionId, credentialOfferSession)
150150
} else {
151-
credentialOfferSession.authorizationDetails = authRequestData.authorization_details
151+
credentialOfferSession.authorizationDetails = authRequestData?.authorization_details
152152
credentialOfferSession.authorizationCode = request.code
153153
credentialOfferSession.status = IssueStatus.ACCESS_TOKEN_REQUESTED
154154
credentialOfferSession.lastUpdatedAt = Date.now()

0 commit comments

Comments
 (0)