Skip to content
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
13 changes: 6 additions & 7 deletions packages/callback-example/lib/__tests__/issuerCallback.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { KeyObject } from 'crypto'

import { uuidv4 } from '@sphereon/oid4vc-common'
import {
CredentialRequestClientBuilder,
CredentialRequestClientBuilderV1_0_15,
ProofOfPossessionBuilder
} from '@sphereon/oid4vci-client'
import { CredentialRequestClientBuilderV1_0_15, ProofOfPossessionBuilder } from '@sphereon/oid4vci-client'
import {
Alg,
CNonceState,
Expand All @@ -21,7 +17,6 @@ import {
} from '@sphereon/oid4vci-common'
import {
AuthorizationServerMetadataBuilder,
CredentialDataSupplierResult,
CredentialSupportedBuilderV1_15,
MemoryStates,
VcIssuer,
Expand Down Expand Up @@ -172,7 +167,7 @@ describe('issuerCallback', () => {
})

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

const credentialResponse = await vcIssuer.issueCredential({
credentialRequest: credentialRequest,
issuerCorrelation: {
preAuthorizedCode: 'test_code',
issuerState: 'existing-state'
},
credential,
responseCNonce: state,
credentialSignerCallback: getIssuerCallbackV1_0_15(credential, credentialRequest, didKey.keyPairs, didKey.didDocument.verificationMethod[0].id)
Expand Down
4 changes: 4 additions & 0 deletions packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,10 @@ export class OpenID4VCIClient {
this._state.accessTokenResponse = response.successBody
this._state.dpopResponseParams = response.params
this._state.accessToken = response.successBody.access_token

if (response.successBody.c_nonce) {
this._state.cachedCNonce = response.successBody.c_nonce
}
}

return { ...this.accessTokenResponse, ...(this.dpopResponseParams && { params: this.dpopResponseParams }) }
Expand Down
4 changes: 4 additions & 0 deletions packages/client/lib/OpenID4VCIClientV1_0_15.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,10 @@ export class OpenID4VCIClientV1_0_15 {
this._state.accessTokenResponse = response.successBody
this._state.dpopResponseParams = response.params
this._state.accessToken = response.successBody.access_token

if (response.successBody.c_nonce) {
this._state.cachedCNonce = response.successBody.c_nonce
}
}

return { ...this.accessTokenResponse, ...(this.dpopResponseParams && { params: this.dpopResponseParams }) }
Expand Down
6 changes: 6 additions & 0 deletions packages/client/lib/__tests__/SdJwt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ describe('sd-jwt vc', () => {
.reply(200, async (_, body) =>
vcIssuer.issueCredential({
credentialRequest: { ...(body as any), credential_identifier: 'SdJwtCredential' },
issuerCorrelation: {
preAuthorizedCode: '123'
},
credential: {
vct: 'Hello',
iss: 'did:example:123',
Expand Down Expand Up @@ -259,6 +262,9 @@ describe('sd-jwt vc', () => {
.reply(200, async (_, body) =>
vcIssuer.issueCredential({
credentialRequest: { ...(body as any), credential_identifier: offered.vct },
issuerCorrelation: {
preAuthorizedCode: '123'
},
credential: {
vct: 'Hello',
iss: 'example.com',
Expand Down
2 changes: 1 addition & 1 deletion packages/did-auth-siop-adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
},
"dependencies": {
"@sphereon/did-auth-siop": "workspace:^",
"@sphereon/did-uni-client": "^0.6.2",
"@sphereon/did-uni-client": "^0.6.4",
"@sphereon/oid4vc-common": "workspace:^",
"@sphereon/wellknown-dids-client": "^0.1.3",
"did-jwt": "6.11.6",
Expand Down
38 changes: 33 additions & 5 deletions packages/issuer-rest/lib/oid4vci-api-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
validateJWT,
WellKnownEndpoints
} from '@sphereon/oid4vci-common'
import { ITokenEndpointOpts, LOG, VcIssuer } from '@sphereon/oid4vci-issuer'
import { IssuerCorrelation, ITokenEndpointOpts, LOG, VcIssuer } from '@sphereon/oid4vci-issuer'
import { env, ISingleEndpointOpts, sendErrorResponse } from '@sphereon/ssi-express-support'
import { InitiatorType, SubSystem, System } from '@sphereon/ssi-types'
import { NextFunction, Request, Response, Router } from 'express'
Expand Down Expand Up @@ -302,9 +302,17 @@ export function getCredentialEndpoint(
try {
const credentialRequest = request.body as CredentialRequestV1_0_15
LOG.log(`credential request received`, credentialRequest)
const issuerCorrelation :IssuerCorrelation = {}
try {
const jwt = extractBearerToken(request.header('Authorization'))
await validateJWT(jwt, { accessTokenVerificationCallback: opts.accessTokenVerificationCallback ?? issuer.jwtVerifyCallback })
const jwtVerifyResult = (await validateJWT(jwt, { accessTokenVerificationCallback: opts.accessTokenVerificationCallback ?? issuer.jwtVerifyCallback }))
const tokenClaims = jwtVerifyResult.jwt.payload
if('preAuthorizedCode' in tokenClaims && typeof tokenClaims.preAuthorizedCode === 'string') {
issuerCorrelation.preAuthorizedCode = tokenClaims.preAuthorizedCode
}
if('issuer_state' in tokenClaims && typeof tokenClaims.issuer_state === 'string') {
issuerCorrelation.issuerState = tokenClaims.issuer_state
}
} catch (e) {
LOG.warning(e)
return sendErrorResponse(response, 400, {
Expand All @@ -314,6 +322,7 @@ export function getCredentialEndpoint(

const credential = await issuer.issueCredential({
credentialRequest: credentialRequest,
issuerCorrelation,
tokenExpiresIn: opts.tokenExpiresIn,
cNonceExpiresIn: opts.cNonceExpiresIn
})
Expand Down Expand Up @@ -425,13 +434,21 @@ export function nonceEndpoint(router: Router, issuer: VcIssuer, opts: INonceEndp

router.post(path, async (request: Request, response: Response) => {
try {
let preAuthorizedCode: string | undefined
let issuerState: string | undefined

// Verify access token if present (optional per spec)
// If not present, the nonce will be unbound to any session
if (request.header('Authorization')) {
try {
const jwt = extractBearerToken(request.header('Authorization'))
await validateJWT(jwt, {
const jwtResult = await validateJWT(jwt, {
accessTokenVerificationCallback: issuer.jwtVerifyCallback
})

// Extract session info from access token
const accessToken = jwtResult.jwt.payload as AccessTokenRequest
preAuthorizedCode = accessToken['pre-authorized_code']
} catch (e) {
LOG.warning(e)
return sendErrorResponse(response, 400, {
Expand All @@ -444,11 +461,22 @@ export function nonceEndpoint(router: Router, issuer: VcIssuer, opts: INonceEndp
const cNonceExpiresIn = issuer.cNonceExpiresIn || 300

const createdAt = epochTime()
await issuer.cNonces.set(cNonce, {

// Create nonce state - only include session identifiers if available
const cNonceState: any = {
cNonce,
createdAt: createdAt,
expiresAt: createdAt + cNonceExpiresIn
})
}

if (preAuthorizedCode) {
cNonceState.preAuthorizedCode = preAuthorizedCode
}
if (issuerState) {
cNonceState.issuerState = issuerState
}

await issuer.cNonces.set(cNonce, cNonceState)

return response.json({
c_nonce: cNonce,
Expand Down
60 changes: 33 additions & 27 deletions packages/issuer/lib/VcIssuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import {
CredentialDataSupplier,
CredentialDataSupplierArgs,
CredentialIssuanceInput,
CredentialSignerCallback
CredentialSignerCallback, IssuerCorrelation
} from './types'

import { LOG } from './index'
Expand Down Expand Up @@ -367,6 +367,7 @@ export class VcIssuer {
*/
public async issueCredential(opts: {
credentialRequest: CredentialRequest
issuerCorrelation: IssuerCorrelation
credential?: CredentialIssuanceInput
credentialDataSupplier?: CredentialDataSupplier
credentialDataSupplierInput?: CredentialDataSupplierInput
Expand All @@ -381,17 +382,16 @@ export class VcIssuer {
throw new Error('credential request should be of spec version 1.0.13 or above')
}*/
const credentialRequest = opts.credentialRequest as CredentialRequestV1_0_15
let preAuthorizedCode: string | undefined
let issuerState: string | undefined
const issuerCorrelation = opts.issuerCorrelation
try {
if (!('credential_identifier' in credentialRequest) && !('credential_configuration_id' in credentialRequest)) {
throw new Error('credential request should have either credential_identifier or credential_configuration_id')
throw Error('credential request should have either credential_identifier or credential_configuration_id')
}

// Validate the credential_configuration_id exists in metadata if used
if ('credential_configuration_id' in credentialRequest && credentialRequest.credential_configuration_id) {
if (!this._issuerMetadata.credential_configurations_supported?.[credentialRequest.credential_configuration_id]) {
throw new Error(TokenErrorResponse.invalid_request)
throw Error(TokenErrorResponse.invalid_request)
}
}
let format = this.lookupCredentialFormat(credentialRequest)
Expand All @@ -400,8 +400,12 @@ export class VcIssuer {
format,
tokenExpiresIn: opts.tokenExpiresIn ?? 180
})
preAuthorizedCode = validated.preAuthorizedCode
issuerState = validated.issuerState
if(validated.preAuthorizedCode && !issuerCorrelation.preAuthorizedCode) {
issuerCorrelation.preAuthorizedCode = validated.preAuthorizedCode
}
if(validated.issuerState && !issuerCorrelation.issuerState) {
issuerCorrelation.issuerState = validated.issuerState
}

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

let signerCallback: CredentialSignerCallback | undefined = opts.credentialSignerCallback
const session: CredentialOfferSession | undefined = preAuthorizedCode && preAuthSession ? preAuthSession : authSession
const session: CredentialOfferSession | undefined = issuerCorrelation.preAuthorizedCode && preAuthSession ? preAuthSession : authSession
if (opts.credential) {
credential = opts.credential
} else {
Expand Down Expand Up @@ -521,17 +525,17 @@ export class VcIssuer {

let notification_id: string | undefined

if (preAuthorizedCode && preAuthSession) {
if (issuerCorrelation.preAuthorizedCode && preAuthSession) {
preAuthSession.lastUpdatedAt = +new Date()
preAuthSession.status = IssueStatus.CREDENTIAL_ISSUED
notification_id = preAuthSession.notification_id
await this._credentialOfferSessions.set(preAuthorizedCode, preAuthSession)
} else if (issuerState && authSession) {
await this._credentialOfferSessions.set(issuerCorrelation.preAuthorizedCode, preAuthSession)
} else if (issuerCorrelation.issuerState && authSession) {
// If both were set we used the pre auth flow above as well, hence the else if
authSession.lastUpdatedAt = +new Date()
authSession.status = IssueStatus.CREDENTIAL_ISSUED
notification_id = authSession.notification_id
await this._credentialOfferSessions.set(issuerState, authSession)
await this._credentialOfferSessions.set(issuerCorrelation.issuerState, authSession)
}

const response: CredentialResponse = {
Expand All @@ -553,7 +557,7 @@ export class VcIssuer {
}
return response
} catch (error: unknown) {
await this.updateSession({ preAuthorizedCode, issuerState, error })
await this.updateSession({ preAuthorizedCode: issuerCorrelation.preAuthorizedCode, issuerState: issuerCorrelation.issuerState, error })
throw error
}
}
Expand Down Expand Up @@ -651,18 +655,19 @@ export class VcIssuer {

private async validateCredentialRequestProof({
credentialRequest,
issuerCorrelation,
format,
jwtVerifyCallback,
tokenExpiresIn
}: {
credentialRequest: CredentialRequest,
issuerCorrelation: IssuerCorrelation
format?: OID4VCICredentialFormat,
tokenExpiresIn: number // expiration duration in seconds
// grants?: Grant,
clientId?: string
jwtVerifyCallback?: JWTVerifyCallback
}) {
let preAuthorizedCode: string | undefined
let issuerState: string | undefined

const supportedIssuanceFormats = ['jwt_vc_json', 'jwt_vc_json-ld', 'dc+sd-jwt', 'ldp_vc', 'mso_mdoc']
Expand All @@ -683,24 +688,24 @@ export class VcIssuer {
const { didDocument, did, jwt } = jwtVerifyResult
const { header, payload } = jwt
const { iss, aud, iat, nonce } = payload
const issuer_state = 'issuer_state' in credentialRequest && credentialRequest.issuer_state ? credentialRequest.issuer_state : undefined
const issuer_state = 'issuer_state' in credentialRequest && credentialRequest.issuer_state
? credentialRequest.issuer_state : issuerCorrelation.issuerState
if (!nonce && !issuer_state) {
throw Error('No nonce was found in the Proof of Possession')
throw Error('No nonce or issuer_state was found in the Proof of Possession')
}
let createdAt: number

let createdAt: number = +new Date()
let cNonceState: CNonceState | undefined
if (nonce) {
cNonceState = await this.cNonces.getAsserted(nonce)
preAuthorizedCode = cNonceState.preAuthorizedCode
issuerState = cNonceState.issuerState
createdAt = cNonceState.createdAt
} else if (issuer_state) {
}
if (issuer_state) {
const session = await this._credentialOfferSessions.getAsserted(issuer_state as string)
issuerState = issuer_state as string | undefined
createdAt = session.createdAt
} else {
throw Error('No nonce or issuer_state was found in the Proof of Possession')
}

// The verify callback should set the correct values, but let's look at the JWT ourselves to to be sure
const alg = jwtVerifyResult.alg ?? header.alg
const kid = jwtVerifyResult.kid ?? header.kid
Expand Down Expand Up @@ -728,18 +733,19 @@ export class VcIssuer {
throw Error(DID_NO_DIDDOC_ERROR)
}

const preAuthSession = preAuthorizedCode ? await this.credentialOfferSessions.get(preAuthorizedCode) : undefined
const preAuthSession = issuerCorrelation.preAuthorizedCode
? await this.credentialOfferSessions.get(issuerCorrelation.preAuthorizedCode) : undefined
const authSession = issuerState ? await this.credentialOfferSessions.get(issuerState) : undefined
if (!preAuthSession && !authSession) {
throw Error('Either a pre-authorized code or issuer state needs to be present')
}
if (preAuthSession) {
if (!preAuthSession.preAuthorizedCode || preAuthSession.preAuthorizedCode !== preAuthorizedCode) {
if (!preAuthSession.preAuthorizedCode || preAuthSession.preAuthorizedCode !== issuerCorrelation.preAuthorizedCode) {
throw Error('Invalid pre-authorized code')
}
preAuthSession.lastUpdatedAt = +new Date()
preAuthSession.status = IssueStatus.CREDENTIAL_REQUEST_RECEIVED
await this._credentialOfferSessions.set(preAuthorizedCode, preAuthSession)
await this._credentialOfferSessions.set(issuerCorrelation.preAuthorizedCode, preAuthSession)
}
if (authSession) {
if (!authSession.issuerState || authSession.issuerState !== issuerState) {
Expand Down Expand Up @@ -781,9 +787,9 @@ export class VcIssuer {
}
// todo: Add a check of iat against current TS on server with a skew

return { jwtVerifyResult, preAuthorizedCode, preAuthSession, issuerState, authSession, cNonceState }
return { jwtVerifyResult, preAuthorizedCode: issuerCorrelation.preAuthorizedCode, preAuthSession, issuerState, authSession, cNonceState }
} catch (error: unknown) {
await this.updateSession({ preAuthorizedCode, issuerState, error })
await this.updateSession({ preAuthorizedCode: issuerCorrelation.preAuthorizedCode, issuerState, error })
throw error
}
}
Expand Down
Loading