Skip to content

Commit 9cf0d8a

Browse files
authored
Merge pull request #207 from Sphereon-Opensource/fix/SSISDK-73_nonce-session
fix/SSISDK-73_nonce-session
2 parents 2f334a1 + 3428567 commit 9cf0d8a

13 files changed

Lines changed: 122 additions & 58 deletions

File tree

packages/callback-example/lib/__tests__/issuerCallback.spec.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import { KeyObject } from 'crypto'
22

33
import { uuidv4 } from '@sphereon/oid4vc-common'
4-
import {
5-
CredentialRequestClientBuilder,
6-
CredentialRequestClientBuilderV1_0_15,
7-
ProofOfPossessionBuilder
8-
} from '@sphereon/oid4vci-client'
4+
import { CredentialRequestClientBuilderV1_0_15, ProofOfPossessionBuilder } from '@sphereon/oid4vci-client'
95
import {
106
Alg,
117
CNonceState,
@@ -21,7 +17,6 @@ import {
2117
} from '@sphereon/oid4vci-common'
2218
import {
2319
AuthorizationServerMetadataBuilder,
24-
CredentialDataSupplierResult,
2520
CredentialSupportedBuilderV1_15,
2621
MemoryStates,
2722
VcIssuer,
@@ -172,7 +167,7 @@ describe('issuerCallback', () => {
172167
})
173168

174169
const nonces = new MemoryStates<CNonceState>()
175-
await nonces.set('test_value', { cNonce: 'test_value', createdAt: +new Date(), issuerState: 'existing-state' })
170+
await nonces.set('test_value', { cNonce: 'test_value', createdAt: +new Date() })
176171
vcIssuer = new VcIssuerBuilder()
177172
.withAuthorizationServers('https://authorization-server')
178173
.withCredentialEndpoint('https://credential-endpoint')
@@ -309,6 +304,10 @@ describe('issuerCallback', () => {
309304

310305
const credentialResponse = await vcIssuer.issueCredential({
311306
credentialRequest: credentialRequest,
307+
issuerCorrelation: {
308+
preAuthorizedCode: 'test_code',
309+
issuerState: 'existing-state'
310+
},
312311
credential,
313312
responseCNonce: state,
314313
credentialSignerCallback: getIssuerCallbackV1_0_15(credential, credentialRequest, didKey.keyPairs, didKey.didDocument.verificationMethod[0].id)

packages/client/lib/OpenID4VCIClient.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,10 @@ export class OpenID4VCIClient {
373373
this._state.accessTokenResponse = response.successBody
374374
this._state.dpopResponseParams = response.params
375375
this._state.accessToken = response.successBody.access_token
376+
377+
if (response.successBody.c_nonce) {
378+
this._state.cachedCNonce = response.successBody.c_nonce
379+
}
376380
}
377381

378382
return { ...this.accessTokenResponse, ...(this.dpopResponseParams && { params: this.dpopResponseParams }) }

packages/client/lib/OpenID4VCIClientV1_0_15.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,10 @@ export class OpenID4VCIClientV1_0_15 {
406406
this._state.accessTokenResponse = response.successBody
407407
this._state.dpopResponseParams = response.params
408408
this._state.accessToken = response.successBody.access_token
409+
410+
if (response.successBody.c_nonce) {
411+
this._state.cachedCNonce = response.successBody.c_nonce
412+
}
409413
}
410414

411415
return { ...this.accessTokenResponse, ...(this.dpopResponseParams && { params: this.dpopResponseParams }) }

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ describe('sd-jwt vc', () => {
151151
.reply(200, async (_, body) =>
152152
vcIssuer.issueCredential({
153153
credentialRequest: { ...(body as any), credential_identifier: 'SdJwtCredential' },
154+
issuerCorrelation: {
155+
preAuthorizedCode: '123'
156+
},
154157
credential: {
155158
vct: 'Hello',
156159
iss: 'did:example:123',
@@ -259,6 +262,9 @@ describe('sd-jwt vc', () => {
259262
.reply(200, async (_, body) =>
260263
vcIssuer.issueCredential({
261264
credentialRequest: { ...(body as any), credential_identifier: offered.vct },
265+
issuerCorrelation: {
266+
preAuthorizedCode: '123'
267+
},
262268
credential: {
263269
vct: 'Hello',
264270
iss: 'example.com',

packages/did-auth-siop-adapter/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
},
2525
"dependencies": {
2626
"@sphereon/did-auth-siop": "workspace:^",
27-
"@sphereon/did-uni-client": "^0.6.2",
27+
"@sphereon/did-uni-client": "^0.6.4",
2828
"@sphereon/oid4vc-common": "workspace:^",
2929
"@sphereon/wellknown-dids-client": "^0.1.3",
3030
"did-jwt": "6.11.6",

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

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
validateJWT,
3030
WellKnownEndpoints
3131
} from '@sphereon/oid4vci-common'
32-
import { ITokenEndpointOpts, LOG, VcIssuer } from '@sphereon/oid4vci-issuer'
32+
import { IssuerCorrelation, ITokenEndpointOpts, LOG, VcIssuer } from '@sphereon/oid4vci-issuer'
3333
import { env, ISingleEndpointOpts, sendErrorResponse } from '@sphereon/ssi-express-support'
3434
import { InitiatorType, SubSystem, System } from '@sphereon/ssi-types'
3535
import { NextFunction, Request, Response, Router } from 'express'
@@ -302,9 +302,17 @@ export function getCredentialEndpoint(
302302
try {
303303
const credentialRequest = request.body as CredentialRequestV1_0_15
304304
LOG.log(`credential request received`, credentialRequest)
305+
const issuerCorrelation :IssuerCorrelation = {}
305306
try {
306307
const jwt = extractBearerToken(request.header('Authorization'))
307-
await validateJWT(jwt, { accessTokenVerificationCallback: opts.accessTokenVerificationCallback ?? issuer.jwtVerifyCallback })
308+
const jwtVerifyResult = (await validateJWT(jwt, { accessTokenVerificationCallback: opts.accessTokenVerificationCallback ?? issuer.jwtVerifyCallback }))
309+
const tokenClaims = jwtVerifyResult.jwt.payload
310+
if('preAuthorizedCode' in tokenClaims && typeof tokenClaims.preAuthorizedCode === 'string') {
311+
issuerCorrelation.preAuthorizedCode = tokenClaims.preAuthorizedCode
312+
}
313+
if('issuer_state' in tokenClaims && typeof tokenClaims.issuer_state === 'string') {
314+
issuerCorrelation.issuerState = tokenClaims.issuer_state
315+
}
308316
} catch (e) {
309317
LOG.warning(e)
310318
return sendErrorResponse(response, 400, {
@@ -314,6 +322,7 @@ export function getCredentialEndpoint(
314322

315323
const credential = await issuer.issueCredential({
316324
credentialRequest: credentialRequest,
325+
issuerCorrelation,
317326
tokenExpiresIn: opts.tokenExpiresIn,
318327
cNonceExpiresIn: opts.cNonceExpiresIn
319328
})
@@ -425,13 +434,21 @@ export function nonceEndpoint(router: Router, issuer: VcIssuer, opts: INonceEndp
425434

426435
router.post(path, async (request: Request, response: Response) => {
427436
try {
437+
let preAuthorizedCode: string | undefined
438+
let issuerState: string | undefined
439+
428440
// Verify access token if present (optional per spec)
441+
// If not present, the nonce will be unbound to any session
429442
if (request.header('Authorization')) {
430443
try {
431444
const jwt = extractBearerToken(request.header('Authorization'))
432-
await validateJWT(jwt, {
445+
const jwtResult = await validateJWT(jwt, {
433446
accessTokenVerificationCallback: issuer.jwtVerifyCallback
434447
})
448+
449+
// Extract session info from access token
450+
const accessToken = jwtResult.jwt.payload as AccessTokenRequest
451+
preAuthorizedCode = accessToken['pre-authorized_code']
435452
} catch (e) {
436453
LOG.warning(e)
437454
return sendErrorResponse(response, 400, {
@@ -444,11 +461,22 @@ export function nonceEndpoint(router: Router, issuer: VcIssuer, opts: INonceEndp
444461
const cNonceExpiresIn = issuer.cNonceExpiresIn || 300
445462

446463
const createdAt = epochTime()
447-
await issuer.cNonces.set(cNonce, {
464+
465+
// Create nonce state - only include session identifiers if available
466+
const cNonceState: any = {
448467
cNonce,
449468
createdAt: createdAt,
450469
expiresAt: createdAt + cNonceExpiresIn
451-
})
470+
}
471+
472+
if (preAuthorizedCode) {
473+
cNonceState.preAuthorizedCode = preAuthorizedCode
474+
}
475+
if (issuerState) {
476+
cNonceState.issuerState = issuerState
477+
}
478+
479+
await issuer.cNonces.set(cNonce, cNonceState)
452480

453481
return response.json({
454482
c_nonce: cNonce,

packages/issuer/lib/VcIssuer.ts

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ import {
6363
CredentialDataSupplier,
6464
CredentialDataSupplierArgs,
6565
CredentialIssuanceInput,
66-
CredentialSignerCallback
66+
CredentialSignerCallback, IssuerCorrelation
6767
} from './types'
6868

6969
import { LOG } from './index'
@@ -367,6 +367,7 @@ export class VcIssuer {
367367
*/
368368
public async issueCredential(opts: {
369369
credentialRequest: CredentialRequest
370+
issuerCorrelation: IssuerCorrelation
370371
credential?: CredentialIssuanceInput
371372
credentialDataSupplier?: CredentialDataSupplier
372373
credentialDataSupplierInput?: CredentialDataSupplierInput
@@ -381,17 +382,16 @@ export class VcIssuer {
381382
throw new Error('credential request should be of spec version 1.0.13 or above')
382383
}*/
383384
const credentialRequest = opts.credentialRequest as CredentialRequestV1_0_15
384-
let preAuthorizedCode: string | undefined
385-
let issuerState: string | undefined
385+
const issuerCorrelation = opts.issuerCorrelation
386386
try {
387387
if (!('credential_identifier' in credentialRequest) && !('credential_configuration_id' in credentialRequest)) {
388-
throw new Error('credential request should have either credential_identifier or credential_configuration_id')
388+
throw Error('credential request should have either credential_identifier or credential_configuration_id')
389389
}
390390

391391
// Validate the credential_configuration_id exists in metadata if used
392392
if ('credential_configuration_id' in credentialRequest && credentialRequest.credential_configuration_id) {
393393
if (!this._issuerMetadata.credential_configurations_supported?.[credentialRequest.credential_configuration_id]) {
394-
throw new Error(TokenErrorResponse.invalid_request)
394+
throw Error(TokenErrorResponse.invalid_request)
395395
}
396396
}
397397
let format = this.lookupCredentialFormat(credentialRequest)
@@ -400,8 +400,12 @@ export class VcIssuer {
400400
format,
401401
tokenExpiresIn: opts.tokenExpiresIn ?? 180
402402
})
403-
preAuthorizedCode = validated.preAuthorizedCode
404-
issuerState = validated.issuerState
403+
if(validated.preAuthorizedCode && !issuerCorrelation.preAuthorizedCode) {
404+
issuerCorrelation.preAuthorizedCode = validated.preAuthorizedCode
405+
}
406+
if(validated.issuerState && !issuerCorrelation.issuerState) {
407+
issuerCorrelation.issuerState = validated.issuerState
408+
}
405409

406410
const { preAuthSession, authSession, cNonceState, jwtVerifyResult } = validated
407411
const did = jwtVerifyResult.did
@@ -422,7 +426,7 @@ export class VcIssuer {
422426
let credential: CredentialIssuanceInput | undefined
423427

424428
let signerCallback: CredentialSignerCallback | undefined = opts.credentialSignerCallback
425-
const session: CredentialOfferSession | undefined = preAuthorizedCode && preAuthSession ? preAuthSession : authSession
429+
const session: CredentialOfferSession | undefined = issuerCorrelation.preAuthorizedCode && preAuthSession ? preAuthSession : authSession
426430
if (opts.credential) {
427431
credential = opts.credential
428432
} else {
@@ -521,17 +525,17 @@ export class VcIssuer {
521525

522526
let notification_id: string | undefined
523527

524-
if (preAuthorizedCode && preAuthSession) {
528+
if (issuerCorrelation.preAuthorizedCode && preAuthSession) {
525529
preAuthSession.lastUpdatedAt = +new Date()
526530
preAuthSession.status = IssueStatus.CREDENTIAL_ISSUED
527531
notification_id = preAuthSession.notification_id
528-
await this._credentialOfferSessions.set(preAuthorizedCode, preAuthSession)
529-
} else if (issuerState && authSession) {
532+
await this._credentialOfferSessions.set(issuerCorrelation.preAuthorizedCode, preAuthSession)
533+
} else if (issuerCorrelation.issuerState && authSession) {
530534
// If both were set we used the pre auth flow above as well, hence the else if
531535
authSession.lastUpdatedAt = +new Date()
532536
authSession.status = IssueStatus.CREDENTIAL_ISSUED
533537
notification_id = authSession.notification_id
534-
await this._credentialOfferSessions.set(issuerState, authSession)
538+
await this._credentialOfferSessions.set(issuerCorrelation.issuerState, authSession)
535539
}
536540

537541
const response: CredentialResponse = {
@@ -553,7 +557,7 @@ export class VcIssuer {
553557
}
554558
return response
555559
} catch (error: unknown) {
556-
await this.updateSession({ preAuthorizedCode, issuerState, error })
560+
await this.updateSession({ preAuthorizedCode: issuerCorrelation.preAuthorizedCode, issuerState: issuerCorrelation.issuerState, error })
557561
throw error
558562
}
559563
}
@@ -651,18 +655,19 @@ export class VcIssuer {
651655

652656
private async validateCredentialRequestProof({
653657
credentialRequest,
658+
issuerCorrelation,
654659
format,
655660
jwtVerifyCallback,
656661
tokenExpiresIn
657662
}: {
658663
credentialRequest: CredentialRequest,
664+
issuerCorrelation: IssuerCorrelation
659665
format?: OID4VCICredentialFormat,
660666
tokenExpiresIn: number // expiration duration in seconds
661667
// grants?: Grant,
662668
clientId?: string
663669
jwtVerifyCallback?: JWTVerifyCallback
664670
}) {
665-
let preAuthorizedCode: string | undefined
666671
let issuerState: string | undefined
667672

668673
const supportedIssuanceFormats = ['jwt_vc_json', 'jwt_vc_json-ld', 'dc+sd-jwt', 'ldp_vc', 'mso_mdoc']
@@ -683,24 +688,24 @@ export class VcIssuer {
683688
const { didDocument, did, jwt } = jwtVerifyResult
684689
const { header, payload } = jwt
685690
const { iss, aud, iat, nonce } = payload
686-
const issuer_state = 'issuer_state' in credentialRequest && credentialRequest.issuer_state ? credentialRequest.issuer_state : undefined
691+
const issuer_state = 'issuer_state' in credentialRequest && credentialRequest.issuer_state
692+
? credentialRequest.issuer_state : issuerCorrelation.issuerState
687693
if (!nonce && !issuer_state) {
688-
throw Error('No nonce was found in the Proof of Possession')
694+
throw Error('No nonce or issuer_state was found in the Proof of Possession')
689695
}
690-
let createdAt: number
696+
697+
let createdAt: number = +new Date()
691698
let cNonceState: CNonceState | undefined
692699
if (nonce) {
693700
cNonceState = await this.cNonces.getAsserted(nonce)
694-
preAuthorizedCode = cNonceState.preAuthorizedCode
695-
issuerState = cNonceState.issuerState
696701
createdAt = cNonceState.createdAt
697-
} else if (issuer_state) {
702+
}
703+
if (issuer_state) {
698704
const session = await this._credentialOfferSessions.getAsserted(issuer_state as string)
699705
issuerState = issuer_state as string | undefined
700706
createdAt = session.createdAt
701-
} else {
702-
throw Error('No nonce or issuer_state was found in the Proof of Possession')
703707
}
708+
704709
// The verify callback should set the correct values, but let's look at the JWT ourselves to to be sure
705710
const alg = jwtVerifyResult.alg ?? header.alg
706711
const kid = jwtVerifyResult.kid ?? header.kid
@@ -728,18 +733,19 @@ export class VcIssuer {
728733
throw Error(DID_NO_DIDDOC_ERROR)
729734
}
730735

731-
const preAuthSession = preAuthorizedCode ? await this.credentialOfferSessions.get(preAuthorizedCode) : undefined
736+
const preAuthSession = issuerCorrelation.preAuthorizedCode
737+
? await this.credentialOfferSessions.get(issuerCorrelation.preAuthorizedCode) : undefined
732738
const authSession = issuerState ? await this.credentialOfferSessions.get(issuerState) : undefined
733739
if (!preAuthSession && !authSession) {
734740
throw Error('Either a pre-authorized code or issuer state needs to be present')
735741
}
736742
if (preAuthSession) {
737-
if (!preAuthSession.preAuthorizedCode || preAuthSession.preAuthorizedCode !== preAuthorizedCode) {
743+
if (!preAuthSession.preAuthorizedCode || preAuthSession.preAuthorizedCode !== issuerCorrelation.preAuthorizedCode) {
738744
throw Error('Invalid pre-authorized code')
739745
}
740746
preAuthSession.lastUpdatedAt = +new Date()
741747
preAuthSession.status = IssueStatus.CREDENTIAL_REQUEST_RECEIVED
742-
await this._credentialOfferSessions.set(preAuthorizedCode, preAuthSession)
748+
await this._credentialOfferSessions.set(issuerCorrelation.preAuthorizedCode, preAuthSession)
743749
}
744750
if (authSession) {
745751
if (!authSession.issuerState || authSession.issuerState !== issuerState) {
@@ -781,9 +787,9 @@ export class VcIssuer {
781787
}
782788
// todo: Add a check of iat against current TS on server with a skew
783789

784-
return { jwtVerifyResult, preAuthorizedCode, preAuthSession, issuerState, authSession, cNonceState }
790+
return { jwtVerifyResult, preAuthorizedCode: issuerCorrelation.preAuthorizedCode, preAuthSession, issuerState, authSession, cNonceState }
785791
} catch (error: unknown) {
786-
await this.updateSession({ preAuthorizedCode, issuerState, error })
792+
await this.updateSession({ preAuthorizedCode: issuerCorrelation.preAuthorizedCode, issuerState, error })
787793
throw error
788794
}
789795
}

0 commit comments

Comments
 (0)