Skip to content

Commit

Permalink
feature: Return a correlationId for a credential offer. Also support …
Browse files Browse the repository at this point in the history
…lookups by issuerState, preAuthCode and correlationId
  • Loading branch information
nklomp committed Feb 16, 2025
1 parent 150a986 commit 64fc725
Show file tree
Hide file tree
Showing 8 changed files with 71 additions and 47 deletions.
8 changes: 2 additions & 6 deletions packages/issuer-rest/lib/OID4VCIServer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {
AuthorizationRequest,
ClientMetadata,
CreateCredentialOfferURIResult,
CredentialConfigurationSupportedV1_0_13,
CredentialOfferMode,
IssuerCredentialSubjectDisplay,
OID4VCICredentialFormat,
QRCodeOpts,
TxCode,
} from '@sphereon/oid4vci-common'
import { CredentialSupportedBuilderV1_13, ITokenEndpointOpts, VcIssuer, VcIssuerBuilder } from '@sphereon/oid4vci-issuer'
import { ExpressSupport, HasEndpointOpts, ISingleEndpointOpts } from '@sphereon/ssi-express-support'
Expand Down Expand Up @@ -82,11 +82,7 @@ function buildVCIFromEnvironment() {
return issuerBuilder.build()
}

export type ICreateCredentialOfferURIResponse = {
uri: string
userPin?: string
txCode?: TxCode
}
export type ICreateCredentialOfferURIResponse = Omit<CreateCredentialOfferURIResult, 'session'>

export interface IGetCredentialOfferEndpointOpts extends ISingleEndpointOpts {
baseUrl: string
Expand Down
4 changes: 2 additions & 2 deletions packages/issuer-rest/lib/oid4vci-api-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export function getCredentialOfferReferenceEndpoint(router: Router, issuer: VcIs
error_description: `query parameter 'id' is missing`,
})
}
const session = await issuer.getCredentialOfferSessionById(id as string, 'id')
const session = await issuer.getCredentialOfferSessionById(id as string, 'correlationId')
if (!session || !session.credentialOffer || session.status !== 'OFFER_CREATED') {
if (session?.status) {
LOG.warning(
Expand Down Expand Up @@ -404,7 +404,7 @@ export function getCredentialOfferEndpoint(router: Router, issuer: VcIssuer, opt
router.get(path, async (request: Request, response: Response) => {
try {
const { id } = request.params
const session = await issuer.getCredentialOfferSessionById(id, 'id')
const session = await issuer.getCredentialOfferSessionById(id, 'correlationId')
if (!session || !session.credentialOffer) {
return sendErrorResponse(response, 404, {
error: 'invalid_request',
Expand Down
63 changes: 39 additions & 24 deletions packages/issuer/lib/VcIssuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { CompactSdJwtVc, CredentialMapper, InitiatorType, SubSystem, System, W3C
import ShortUUID from 'short-uuid'

import { assertValidPinNumber, createCredentialOfferObject, createCredentialOfferURIFromObject, CredentialOfferGrantInput } from './functions'
import { LookupStateManager } from './state-manager'
import { LookupStateManager, MemoryStates } from './state-manager'
import { CredentialDataSupplier, CredentialDataSupplierArgs, CredentialIssuanceInput, CredentialSignerCallback } from './types'

import { LOG } from './index'
Expand All @@ -63,7 +63,7 @@ export class VcIssuer {
private readonly _credentialDataSupplier?: CredentialDataSupplier
private readonly _credentialOfferSessions: IStateManager<CredentialOfferSession>
private readonly _cNonces: IStateManager<CNonceState>
private readonly _uris?: IStateManager<URIState>
private readonly _uris: IStateManager<URIState>
private readonly _cNonceExpiresIn: number
private readonly _asClientOpts?: ClientMetadata

Expand All @@ -87,18 +87,22 @@ export class VcIssuer {
this._issuerMetadata = issuerMetadata
this._authorizationServerMetadata = authorizationServerMetadata
this._defaultCredentialOfferBaseUri = args.defaultCredentialOfferBaseUri
this._credentialOfferSessions = args.credentialOfferSessions
this._credentialOfferSessions = args.credentialOfferSessions ?? new MemoryStates()
this._uris = args.uris ?? new MemoryStates()
this._cNonces = args.cNonces
this._uris = args.uris
this._credentialSignerCallback = args?.credentialSignerCallback
this._jwtVerifyCallback = args?.jwtVerifyCallback
this._credentialDataSupplier = args?.credentialDataSupplier
this._cNonceExpiresIn = (args?.cNonceExpiresIn ?? (process.env.C_NONCE_EXPIRES_IN ? parseInt(process.env.C_NONCE_EXPIRES_IN) : 300)) as number
this._asClientOpts = args?.asClientOpts
}

public async getCredentialOfferSessionById(id: string, lookup?: 'uri' | 'id' | 'preAuthorizedCode'): Promise<CredentialOfferSession> {
if (lookup) {
public async getCredentialOfferSessionById(
id: string,
lookup?: 'uri' | 'preAuthorizedCode' | 'issuerState' | 'correlationId',
): Promise<CredentialOfferSession> {
// preAuth and issuerState can be looked up directly
if (lookup && lookup !== 'preAuthorizedCode' && lookup !== 'issuerState') {
if (!this.uris) {
return Promise.reject(Error('Cannot lookup credential offer by id if URI state manager is not set'))
}
Expand All @@ -111,9 +115,13 @@ export class VcIssuer {
return session
}

public async deleteCredentialOfferSessionById(id: string, lookup: 'uri' | 'id' = 'id'): Promise<CredentialOfferSession> {
public async deleteCredentialOfferSessionById(
id: string,
lookup: 'uri' | 'preAuthorizedCode' | 'issuerState' | 'correlationId' = 'correlationId',
): Promise<CredentialOfferSession> {
const session = await this.getCredentialOfferSessionById(id, lookup)
if (session) {
new LookupStateManager<URIState, CredentialOfferSession>(this.uris, this._credentialOfferSessions, lookup).delete(id)
if (session.preAuthorizedCode && (await this._credentialOfferSessions.has(session.preAuthorizedCode))) {
await this._credentialOfferSessions.delete(session.preAuthorizedCode)
}
Expand Down Expand Up @@ -214,7 +222,7 @@ export class VcIssuer {
}
const createdAt = +new Date()
const lastUpdatedAt = createdAt
const expirationInMs = (opts.sessionLifeTimeInSec ?? 10*60) * 1000
const expirationInMs = (opts.sessionLifeTimeInSec ?? 10 * 60) * 1000
const expiresAt = createdAt + Math.abs(expirationInMs)
if (offerMode === 'REFERENCE') {
if (!this.uris) {
Expand All @@ -233,7 +241,7 @@ export class VcIssuer {
expiresAt,
preAuthorizedCode,
issuerState,
credentialOfferCorrelationId: correlationId,
correlationId: correlationId,
})
}

Expand Down Expand Up @@ -264,38 +272,45 @@ export class VcIssuer {
statusLists: statusListOpts,
}

const uri = createCredentialOfferURIFromObject(credentialOffer, offerMode, { ...opts, baseUri })
if (preAuthorizedCode) {
await this.credentialOfferSessions.set(preAuthorizedCode, session)
const lookupManager = new LookupStateManager<URIState, CredentialOfferSession>(this.uris, this._credentialOfferSessions, 'correlationId')
await lookupManager.setMapped(preAuthorizedCode, { preAuthorizedCode, uri, createdAt, expiresAt, correlationId, issuerState }, session)
// await this.credentialOfferSessions.set(preAuthorizedCode, session)
}
// todo: check whether we could have the same value for issuer state and pre auth code if both are supported.
if (issuerState) {
await this.credentialOfferSessions.set(issuerState, session)
const lookupManager = new LookupStateManager<URIState, CredentialOfferSession>(this.uris, this._credentialOfferSessions, 'correlationId')
await lookupManager.setMapped(issuerState, { preAuthorizedCode, uri, createdAt, expiresAt, correlationId, issuerState }, session)
// await this.credentialOfferSessions.set(issuerState, session)
}

const uri = createCredentialOfferURIFromObject(credentialOffer, offerMode, { ...opts, baseUri })
let qrCodeDataUri: string | undefined
if (opts.qrCodeOpts) {
const { AwesomeQR } = await import('awesome-qr')
const qrCode = new AwesomeQR({ ...opts.qrCodeOpts, text: uri })
qrCodeDataUri = `data:image/png;base64,${(await qrCode.draw())!.toString('base64')}`
}
const credentialOfferResult = {
session,
uri,
qrCodeDataUri,
correlationId,
txCode,
...(userPin !== undefined && { userPin, pinLength: userPin?.length ?? 0 }),
}
EVENTS.emit(CredentialOfferEventNames.OID4VCI_OFFER_CREATED, {
eventName: CredentialOfferEventNames.OID4VCI_OFFER_CREATED,
id: correlationId,
data: uri,
initiator: '<unknown>',
data: credentialOfferResult,
initiator: '<Unknown>',
initiatorType: InitiatorType.EXTERNAL,
system: System.OID4VCI,
// todo: Issuer
issuer: this.issuerMetadata.credential_issuer,
subsystem: SubSystem.API,
createdAt,
expiresAt,
})
return {
session,
uri,
qrCodeDataUri,
txCode,
...(userPin !== undefined && { userPin, pinLength: userPin?.length ?? 0 }),
}
return credentialOfferResult
}

/**
Expand Down Expand Up @@ -755,7 +770,7 @@ export class VcIssuer {
return this._credentialDataSupplier
}

get uris(): IStateManager<URIState> | undefined {
get uris(): IStateManager<URIState> {
return this._uris
}

Expand Down
2 changes: 1 addition & 1 deletion packages/issuer/lib/__tests__/VcIssuer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ describe('VcIssuer without did', () => {
.build()

await expect(vcIssuerWithoutUriState.getCredentialOfferSessionById('https://example.com/some-uri', 'uri')).rejects.toThrow(
'Cannot lookup credential offer by id if URI state manager is not set',
'issuer state or pre-authorized key not found (https://example.com/some-uri)',
)
})
})
36 changes: 24 additions & 12 deletions packages/issuer/lib/state-manager/LookupStateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,32 @@ export class LookupStateManager<K extends StateType, V extends StateType> implem

private async assertedValueId(key: string): Promise<string> {
const prop = this.lookup
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const valueId = await this.keyValueMapper.getAsserted(key).then((keyState) => (prop in keyState ? keyState[prop] : undefined))
const valueId = await this.keyValueMapper
.getAsserted(key)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.then((keyState) => (keyState && prop in keyState ? keyState[prop] : undefined))
if (typeof valueId !== 'string') {
throw Error('no value id could be derived for key' + key)
}
return valueId
}

private async valueId(key: string): Promise<string | undefined> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return (await this.keyValueMapper.get(key).then((keyState) => (prop in keyState ? keyState[prop] : undefined))) as string
const prop = this.lookup
return (
(await this.keyValueMapper
.get(key)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.then((keyState) => (keyState && prop in keyState ? keyState[prop] : undefined))) as string
)
}

async delete(id: string): Promise<boolean> {
return await this.assertedValueId(id).then(async (value) => {
await this.keyValueMapper.delete(id)
return this.valueStateManager.delete(value)
return await this.valueStateManager.delete(value)
})
}

Expand All @@ -62,13 +69,18 @@ export class LookupStateManager<K extends StateType, V extends StateType> implem
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async set(id: string, stateValue: V): Promise<void> {
throw Error(`Please use the set method that accepts both and id, value and object`)
async set(_id: string, _stateValue: V): Promise<void> {
throw Error(`Please use the setMappedMethod that accepts both and id, value and object`)
}

async setMapped(id: string, keyValue: K, stateValue: V): Promise<void> {
await this.keyValueMapper.set(id, keyValue)
await this.valueStateManager.set(id, stateValue)
async setMapped(valueKey: string, keyObject: K, stateValue: V): Promise<void> {
const keys = keyObject as any
if (!(this.lookup in keys) || !keys[this.lookup]) {
return Promise.reject(new Error(`keyValue ${keyObject} does not contain the lookup property ${this.lookup}`))
}
const key = keys[this.lookup]
await this.keyValueMapper.set(key, keyObject)
await this.valueStateManager.set(valueKey, stateValue)
}

async getAsserted(id: string): Promise<V> {
Expand Down
1 change: 1 addition & 0 deletions packages/oid4vci-common/lib/types/Generic.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export type CredentialDataSupplierInput = any;

export type CreateCredentialOfferURIResult = {
uri: string;
correlationId: string;
qrCodeDataUri?: string;
session: CredentialOfferSession;
userPin?: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/oid4vci-common/lib/types/StateManager.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface URIState extends StateType {
issuerState?: string; //todo: Probably good to hash it here, since it would come in from the client and we could match the hash and thus use the client value
preAuthorizedCode?: string; //todo: Probably good to hash it here, since it would come in from the client and we could match the hash and thus use the client value
uri: string; //todo: Probably good to hash it here, since it would come in from the client and we could match the hash and thus use the client value
credentialOfferCorrelationId?: string;
correlationId?: string;
}

export interface IssueStatusResponse {
Expand Down
2 changes: 1 addition & 1 deletion packages/oid4vci-common/lib/types/v1_0_13.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export interface CredentialOfferRESTRequest extends Partial<CredentialOfferPaylo
// auth_session?: string; Would be a nice extension to support, to allow external systems to determine what the auth_session value should be
// @Deprecated use tx_code in the grant object
correlationId?: string;
sessionLifeTimeInSec?: number
sessionLifeTimeInSec?: number;
pinLength?: number;
qrCodeOpts?: QRCodeOpts;
client_id?: string;
Expand Down

0 comments on commit 64fc725

Please sign in to comment.