Skip to content

Commit f5136db

Browse files
committed
Merge remote-tracking branch 'origin/feature/SSISDK-73_auth_detail_fix' into develop
2 parents 4b35515 + 056c868 commit f5136db

15 files changed

Lines changed: 625 additions & 79 deletions

packages/client/lib/AuthorizationCodeClient.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
AuthorizationChallengeCodeResponse,
3-
AuthorizationChallengeRequestOpts,
4-
AuthorizationDetails,
3+
AuthorizationChallengeRequestOpts, AuthorizationDetailsV1_0_15,
54
AuthorizationRequestOpts,
65
CodeChallengeMethod,
76
CommonAuthorizationChallengeRequest,
@@ -194,7 +193,7 @@ export const createAuthorizationRequestUrl = async ({
194193
...(format && { format }),
195194
...(vct && { vct, claims: cred.claims ? removeDisplayAndValueTypes(cred.claims) : undefined }),
196195
...(doctype && { doctype, claims: cred.claims ? removeDisplayAndValueTypes(cred.claims) : undefined })
197-
} as AuthorizationDetails
196+
} as AuthorizationDetailsV1_0_15
198197
})
199198
if (!authorizationDetails || authorizationDetails.length === 0) {
200199
throw Error(`Could not create authorization details from credential offer. Please pass in explicit details`)
@@ -286,8 +285,8 @@ const hasCredentialDefinition = (cred: any): cred is {
286285

287286
const handleAuthorizationDetails = (
288287
endpointMetadata: EndpointMetadataResultV1_0_15,
289-
authorizationDetails?: AuthorizationDetails | AuthorizationDetails[]
290-
): AuthorizationDetails | AuthorizationDetails[] | undefined => {
288+
authorizationDetails?: AuthorizationDetailsV1_0_15 | AuthorizationDetailsV1_0_15[]
289+
): AuthorizationDetailsV1_0_15 | AuthorizationDetailsV1_0_15[] | undefined => {
291290
if (authorizationDetails) {
292291
if (typeof authorizationDetails === 'string') {
293292
// backwards compat for older versions of the lib
@@ -306,7 +305,7 @@ const handleAuthorizationDetails = (
306305

307306
const handleLocations = (
308307
endpointMetadata: EndpointMetadataResultV1_0_15,
309-
authorizationDetails: AuthorizationDetails
308+
authorizationDetails: AuthorizationDetailsV1_0_15
310309
) => {
311310
if (typeof authorizationDetails === 'string') {
312311
// backwards compat for older versions of the lib

packages/client/lib/AuthorizationDetailsBuilder.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import { AuthorizationDetails, AuthorizationDetailsJwtVcJson, OID4VCICredentialFormat } from '@sphereon/oid4vci-common'
1+
import {
2+
AuthorizationDetailsJwtVcJson,
3+
AuthorizationDetailsV1_0_15,
4+
OID4VCICredentialFormat
5+
} from '@sphereon/oid4vci-common'
26

37
//todo: refactor this builder to be able to create ldp details as well
48
export class AuthorizationDetailsBuilder {
5-
private readonly authorizationDetails: Partial<Exclude<AuthorizationDetails, string>>
9+
private readonly authorizationDetails: Partial<Exclude<AuthorizationDetailsV1_0_15, string>>
610

711
constructor() {
8-
this.authorizationDetails = {}
9-
}
10-
11-
withType(type: string): AuthorizationDetailsBuilder {
12-
this.authorizationDetails.type = type
13-
return this
12+
this.authorizationDetails = { type: 'openid_credential' }
1413
}
1514

1615
withFormats(format: OID4VCICredentialFormat): AuthorizationDetailsBuilder {

packages/client/lib/CredentialRequestClient.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { createDPoP, CreateDPoPClientOpts, getCreateDPoPOptions } from '@sphereon/oid4vc-common'
22
import {
33
acquireDeferredCredential,
4-
AuthorizationDetails,
4+
AuthorizationDetailsV1_0_15,
5+
CredentialRequest,
56
CredentialRequestV1_0_15,
67
CredentialResponse,
78
DPoPResponseParams,
@@ -14,7 +15,6 @@ import {
1415
post,
1516
ProofOfPossession,
1617
supportedOID4VCICredentialFormat,
17-
CredentialRequest,
1818
URL_NOT_VALID
1919
} from '@sphereon/oid4vci-common'
2020
import { CredentialFormat, Loggers } from '@sphereon/ssi-types'
@@ -40,7 +40,7 @@ export interface CredentialRequestOpts {
4040
version: OpenId4VCIVersion
4141
subjectIssuance?: ExperimentalSubjectIssuance
4242
issuerState?: string
43-
authorizationDetails?: AuthorizationDetails[]
43+
authorizationDetails?: AuthorizationDetailsV1_0_15[]
4444
}
4545

4646
export type CreateCredentialRequestOpts = {
@@ -72,15 +72,14 @@ export async function buildProof(
7272
return await proofInput.build()
7373
}
7474

75-
function isOpenIdCredentialDetail(ad: AuthorizationDetails): ad is AuthorizationDetails {
75+
function isOpenIdCredentialDetail(ad: AuthorizationDetailsV1_0_15): ad is AuthorizationDetailsV1_0_15 {
7676
return typeof ad === 'object' && ad !== null && ad.type === 'openid_credential'
7777
}
7878

79-
// Update the helper function:
8079
function findAuthorizationDetail(
81-
authorizationDetails: AuthorizationDetails[] | undefined,
80+
authorizationDetails: AuthorizationDetailsV1_0_15[] | undefined,
8281
preferredConfigId?: string
83-
): AuthorizationDetails | undefined {
82+
): AuthorizationDetailsV1_0_15 | undefined {
8483
if (!authorizationDetails) {
8584
return undefined
8685
}
@@ -93,10 +92,20 @@ function findAuthorizationDetail(
9392

9493
// If a preferred config ID is specified, try to find a match
9594
if (preferredConfigId) {
96-
const match = openIdCredentialDetails.find(detail =>
97-
typeof detail === 'object' && detail !== null &&
98-
(detail as any).credential_configuration_id === preferredConfigId
99-
)
95+
const match = openIdCredentialDetails.find(detail => {
96+
if (typeof detail !== 'object' || detail === null) return false
97+
98+
const detailObj = detail as any
99+
100+
if (detailObj.credential_configuration_id === preferredConfigId) {
101+
return true
102+
}
103+
if (detailObj.credential_identifier === preferredConfigId) {
104+
return true
105+
}
106+
return Array.isArray(detailObj.credential_identifiers) && detailObj.credential_identifiers.includes(preferredConfigId)
107+
})
108+
100109
if (match) {
101110
return match
102111
}

packages/client/lib/__tests__/AuthorizationDetailsBuilder.spec.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ describe('AuthorizationDetailsBuilder test', () => {
88
const actual = new AuthorizationDetailsBuilder()
99
.withFormats('jwt_vc' as OID4VCICredentialFormat)
1010
.withLocations(['test1', 'test2'])
11-
.withType('openid_credential')
1211
.buildJwtVcJson()
1312
expect(actual).toEqual({
1413
type: 'openid_credential',
@@ -20,7 +19,6 @@ describe('AuthorizationDetailsBuilder test', () => {
2019
const actual = new AuthorizationDetailsBuilder()
2120
.withFormats('jwt_vc' as OID4VCICredentialFormat)
2221
.withLocations(['test1'])
23-
.withType('openid_credential')
2422
.buildJwtVcJson()
2523
expect(actual).toEqual({
2624
type: 'openid_credential',
@@ -31,24 +29,16 @@ describe('AuthorizationDetailsBuilder test', () => {
3129
it('should create AuthorizationDetails object if locations is missing', () => {
3230
const actual = new AuthorizationDetailsBuilder()
3331
.withFormats('jwt_vc' as OID4VCICredentialFormat)
34-
.withType('openid_credential')
3532
.buildJwtVcJson()
3633
expect(actual).toEqual({
3734
type: 'openid_credential',
3835
format: 'jwt_vc',
3936
})
4037
})
41-
it('should fail if type is missing', () => {
42-
expect(() => {
43-
new AuthorizationDetailsBuilder()
44-
.withFormats('jwt_vc' as OID4VCICredentialFormat)
45-
.withLocations(['test1'])
46-
.buildJwtVcJson()
47-
}).toThrow(Error('Type and format are required properties'))
48-
})
38+
4939
it('should fail if format is missing', () => {
5040
expect(() => {
51-
new AuthorizationDetailsBuilder().withType('openid_credential').withLocations(['test1']).buildJwtVcJson()
41+
new AuthorizationDetailsBuilder().withLocations(['test1']).buildJwtVcJson()
5242
}).toThrow(Error('Type and format are required properties'))
5343
})
5444
})

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
@@ -226,7 +226,11 @@ export class OID4VCIServer {
226226
})
227227
this.assertAccessTokenHandling()
228228
if (!this.isTokenEndpointDisabled(opts?.endpointOpts?.tokenEndpointOpts, opts?.asClientOpts)) {
229-
accessTokenEndpoint(this.router, this.issuer, { ...opts?.endpointOpts?.tokenEndpointOpts, baseUrl: this.baseUrl })
229+
accessTokenEndpoint(this.router, this.issuer, {
230+
...opts?.endpointOpts?.tokenEndpointOpts,
231+
baseUrl: this.baseUrl,
232+
authRequestsData: this.authRequestsData
233+
})
230234
}
231235
if (this.isStatusEndpointEnabled(opts?.endpointOpts?.getStatusOpts)) {
232236
getIssueStatusEndpoint(this.router, this.issuer, { ...opts?.endpointOpts?.getStatusOpts, baseUrl: this.baseUrl })

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

Lines changed: 63 additions & 5 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,
@@ -313,6 +316,23 @@ export function getCredentialEndpoint(
313316
if('issuer_state' in tokenClaims && typeof tokenClaims.issuer_state === 'string') {
314317
issuerCorrelation.issuerState = tokenClaims.issuer_state
315318
}
319+
320+
// Handle credential_identifier from authorization_details flow
321+
if ('authorization_details' in tokenClaims && Array.isArray(tokenClaims.authorization_details)) {
322+
issuerCorrelation.authorizationDetails = tokenClaims.authorization_details
323+
324+
if (credentialRequest.credential_identifier) {
325+
const validIdentifiers = tokenClaims.authorization_details
326+
.flatMap((detail: any) => detail.credential_identifiers || [])
327+
328+
if (!validIdentifiers.includes(credentialRequest.credential_identifier)) {
329+
return sendErrorResponse(response, 400, {
330+
error: 'invalid_credential_request',
331+
error_description: 'credential_identifier not found in authorization_details'
332+
})
333+
}
334+
}
335+
}
316336
} catch (e) {
317337
LOG.warning(e)
318338
return sendErrorResponse(response, 400, {
@@ -670,13 +690,51 @@ export function pushedAuthorizationEndpoint(
670690
})
671691
}
672692

673-
//TODO Implement authorization_details verification
693+
// Add the authorization_details validation here:
694+
if (req.body.authorization_details) {
695+
const authDetails = Array.isArray(req.body.authorization_details)
696+
? req.body.authorization_details
697+
: JSON.parse(req.body.authorization_details)
698+
699+
// Validate each authorization detail
700+
for (const detail of authDetails) {
701+
if (detail.type !== 'openid_credential') {
702+
return sendErrorResponse(res, 400, {
703+
error: 'invalid_authorization_details',
704+
error_description: 'Only openid_credential type is supported'
705+
})
706+
}
707+
708+
// Validate credential_configuration_id exists in issuer metadata
709+
if (detail.credential_configuration_id &&
710+
!issuer.issuerMetadata.credential_configurations_supported[detail.credential_configuration_id]) {
711+
return sendErrorResponse(res, 400, {
712+
error: 'invalid_credential_request',
713+
error_description: `Unsupported credential configuration: ${detail.credential_configuration_id}`
714+
})
715+
}
716+
}
717+
}
674718

675719
// TODO: Both UUID and requestURI need to be configurable for the server
676720
const uuid = uuidv4()
677721
const requestUri = `urn:ietf:params:oauth:request_uri:${uuid}`
678-
// The redirect_uri is created and set in a map, to keep track of the actual request
679-
authRequestsData.set(requestUri, req.body)
722+
723+
// Store authorization_details in the request for later retrieval
724+
let requestData = req.body
725+
if (req.body.authorization_details) {
726+
const authDetails = Array.isArray(req.body.authorization_details)
727+
? req.body.authorization_details
728+
: JSON.parse(req.body.authorization_details)
729+
730+
requestData = {
731+
...req.body,
732+
authorization_details: authDetails // Store parsed authorization details
733+
}
734+
}
735+
736+
authRequestsData.set(requestUri, requestData)
737+
680738
// Invalidates the request_uri removing it from the mapping after it is expired, needs to be refactored because
681739
// some of the properties will be needed in subsequent steps if the authorization succeeds
682740
// TODO in the /token endpoint the code_challenge must be matched against the hashed code_verifier

packages/issuer/lib/VcIssuer.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,17 @@ export class VcIssuer {
394394
throw Error(TokenErrorResponse.invalid_request)
395395
}
396396
}
397+
398+
// Validate credential_identifier against authorization_details if present
399+
if ('credential_identifier' in credentialRequest && credentialRequest.credential_identifier && issuerCorrelation.authorizationDetails) {
400+
const validIdentifiers = issuerCorrelation.authorizationDetails
401+
.flatMap((detail: any) => detail.credential_identifiers || [])
402+
403+
if (!validIdentifiers.includes(credentialRequest.credential_identifier)) {
404+
throw Error('credential_identifier not found in authorization_details')
405+
}
406+
}
407+
397408
let format = this.lookupCredentialFormat(credentialRequest)
398409
const validated = await this.validateCredentialRequestProof({
399410
...opts,

0 commit comments

Comments
 (0)