Skip to content

Commit

Permalink
feat: allow default auth request options for VCI links/machines, like…
Browse files Browse the repository at this point in the history
… clientId and redirectUri
  • Loading branch information
nklomp committed May 29, 2024
1 parent dfc52da commit 434196e
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 102 deletions.
4 changes: 2 additions & 2 deletions packages/oid4vci-holder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"build:clean": "tsc --build --clean && tsc --build"
},
"dependencies": {
"@sphereon/oid4vci-client": "0.10.4-unstable.63+1d87208",
"@sphereon/oid4vci-common": "0.10.4-unstable.63+1d87208",
"@sphereon/oid4vci-client": "0.10.4-unstable.70",
"@sphereon/oid4vci-common": "0.10.4-unstable.70",
"@sphereon/ssi-sdk-ext.did-utils": "0.18.0",
"@sphereon/ssi-sdk-ext.did-resolver-jwk": "0.18.0",
"@sphereon/ssi-sdk.contact-manager": "workspace:*",
Expand Down
74 changes: 49 additions & 25 deletions packages/oid4vci-holder/src/agent/OID4VCIHolder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { OpenID4VCIClient } from '@sphereon/oid4vci-client'
import { CredentialConfigurationSupported, DefaultURISchemes, Jwt, ProofOfPossessionCallbacks } from '@sphereon/oid4vci-common'
import {
AuthorizationRequestOpts,
CredentialConfigurationSupported,
DefaultURISchemes,
Jwt,
ProofOfPossessionCallbacks,
} from '@sphereon/oid4vci-common'
import {
CorrelationIdentifierType,
IBasicCredentialLocaleBranding,
Expand Down Expand Up @@ -109,6 +115,8 @@ export class OID4VCIHolder implements IAgentPlugin {
SignatureAlgorithmEnum.ES256K,
SignatureAlgorithmEnum.EdDSA,
]
private static readonly DEFAULT_MOBILE_REDIRECT_URI = `${DefaultURISchemes.CREDENTIAL_OFFER}://`
private readonly defaultAuthorizationRequestOpts: AuthorizationRequestOpts = { redirectUri: OID4VCIHolder.DEFAULT_MOBILE_REDIRECT_URI }
private readonly onContactIdentityCreated?: (args: OnContactIdentityCreatedArgs) => Promise<void>
private readonly onCredentialStored?: (args: OnCredentialStoredArgs) => Promise<void>
private readonly onIdentifierCreated?: (args: OnIdentifierCreatedArgs) => Promise<void>
Expand All @@ -122,6 +130,7 @@ export class OID4VCIHolder implements IAgentPlugin {
jsonldCryptographicSuitePreferences,
didMethodPreferences,
jwtCryptographicSuitePreferences,
defaultAuthRequestOptions,
} = options ?? {}

if (vcFormatPreferences !== undefined && vcFormatPreferences.length > 0) {
Expand All @@ -136,6 +145,9 @@ export class OID4VCIHolder implements IAgentPlugin {
if (jwtCryptographicSuitePreferences !== undefined && jwtCryptographicSuitePreferences.length > 0) {
this.jwtCryptographicSuitePreferences = jwtCryptographicSuitePreferences
}
if (defaultAuthRequestOptions) {
this.defaultAuthorizationRequestOpts = defaultAuthRequestOptions
}
this.onContactIdentityCreated = onContactIdentityCreated
this.onCredentialStored = onCredentialStored
this.onIdentifierCreated = onIdentifierCreated
Expand Down Expand Up @@ -174,6 +186,7 @@ export class OID4VCIHolder implements IAgentPlugin {

const oid4vciMachineInstanceArgs: OID4VCIMachineInstanceOpts = {
...args,
authorizationRequestOpts: { ...this.defaultAuthorizationRequestOpts, ...args.authorizationRequestOpts },
services: {
...services,
...args.services,
Expand Down Expand Up @@ -205,11 +218,20 @@ export class OID4VCIHolder implements IAgentPlugin {
) {
return Promise.reject(Error(`Invalid OID4VCI credential offer URI: ${requestData?.uri}`))
}
const authorizationRequest = { ...this.defaultAuthorizationRequestOpts, ...args.authorizationRequestOpts } satisfies AuthorizationRequestOpts

if (!authorizationRequest.redirectUri) {
authorizationRequest.redirectUri = OID4VCIHolder.DEFAULT_MOBILE_REDIRECT_URI
}
if (authorizationRequest.redirectUri.startsWith('http') && !authorizationRequest.clientId) {
// At least set a default for a web based wallet.
// TODO: We really need (dynamic) client registration support
authorizationRequest.clientId = authorizationRequest.redirectUri
}

const openID4VCIClient = await OpenID4VCIClient.fromURI({
uri: requestData?.uri,
// TODO: It would be nice to be able to configure the plugin with a custom redirect URI, mainly for mobile
authorizationRequest: { redirectUri: `${DefaultURISchemes.CREDENTIAL_OFFER}://` },
authorizationRequest,
})

const serverMetadata = await openID4VCIClient.retrieveServerMetadata()
Expand Down Expand Up @@ -251,7 +273,7 @@ export class OID4VCIHolder implements IAgentPlugin {
const defaultCredentialType = 'VerifiableCredential'

const credentialType =
credentialConfigSupported.credential_definition.type.find((type: string): boolean => type !== defaultCredentialType) ??
credentialConfigSupported.credential_definition?.type?.find((type: string): boolean => type !== defaultCredentialType) ??
defaultCredentialType
const localeBranding = credentialBranding?.[credentialType]
const credentialAlias = (
Expand Down Expand Up @@ -385,29 +407,31 @@ export class OID4VCIHolder implements IAgentPlugin {
pin,
authorizationResponse: JSON.parse(await client.exportState()).authorizationCodeResponse,
})
if ('credential_definition' in issuanceOpt) {
const credentialType = issuanceOpt.credential_definition.type.length === 1? issuanceOpt.credential_definition.type[0]: issuanceOpt.credential_definition.type.filter(type=>type!='VerifiableCredential')[0]
const credentialResponse = await client.acquireCredentials({
//fixme: this isn't the correct way to handle this. the type is wrong. we're not correctly handling moving from Record<string, CredentialConfigSupport>
credentialType: issuanceOpt.id ?? credentialType,
proofCallbacks: callbacks,
format: issuanceOpt.format,
// TODO: We need to update the machine and add notifications support for actual deferred credentials instead of just waiting/retrying
deferredCredentialAwait: true,
kid,
alg,
jti: uuidv4(),
})
// @ts-ignore
const definition = issuanceOpt.credential_definition
const idFromType =
definition?.type?.length === 1 ? definition?.type[0] : definition?.type?.filter((type: string) => type !== 'VerifiableCredential')[0]
const credentialType = issuanceOpt.credentialConfigurationId ?? issuanceOpt.id ?? idFromType
if (!credentialType) {
throw Error('cannot determine credential id to request')
}
const credentialResponse = await client.acquireCredentials({
credentialType,
proofCallbacks: callbacks,
format: issuanceOpt.format,
// TODO: We need to update the machine and add notifications support for actual deferred credentials instead of just waiting/retrying
deferredCredentialAwait: true,
kid,
alg,
jti: uuidv4(),
})

const credential = {
id: issuanceOpt.id,
issuanceOpt,
credentialResponse,
}
return mapCredentialToAccept({ credential })
} else {
throw new Error('IssuanceOpt is not of version 1.13 or above') // FIXME
const credential = {
id: issuanceOpt.id,
issuanceOpt,
credentialResponse,
}
return mapCredentialToAccept({ credential })
} catch (error) {
return Promise.reject(error)
}
Expand Down
64 changes: 31 additions & 33 deletions packages/oid4vci-holder/src/agent/OID4VCIHolderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,25 +60,27 @@ export const getCredentialBranding = async (args: GetCredentialBrandingArgs): Pr
const { credentialsSupported, context } = args
const credentialBranding: Record<string, Array<IBasicCredentialLocaleBranding>> = {}
await Promise.all(
Object.values(credentialsSupported).map(async (credentialsConfigSupported: CredentialConfigurationSupported): Promise<void> => {
const localeBranding: Array<IBasicCredentialLocaleBranding> = await Promise.all(
(credentialsConfigSupported.display ?? []).map(
async (display: CredentialsSupportedDisplay): Promise<IBasicCredentialLocaleBranding> =>
await context.agent.ibCredentialLocaleBrandingFrom({ localeBranding: await credentialLocaleBrandingFrom(display) }),
),
)

const defaultCredentialType = 'VerifiableCredential'
const credentialTypes: Array<string> =
'types' in credentialsConfigSupported // TODO credentialsConfigSupported.types is deprecated
? (credentialsConfigSupported.types as string[])
: 'credential_definition' in credentialsConfigSupported
? credentialsConfigSupported.credential_definition.type
: [defaultCredentialType]

const filteredCredentialTypes = credentialTypes.filter((type: string): boolean => type !== defaultCredentialType)
credentialBranding[filteredCredentialTypes[0]] = localeBranding // TODO for now taking the first type
}),
Object.entries(credentialsSupported).map(
([configId, _credentialsSupported]) =>
async (credentialsConfigSupported: CredentialConfigurationSupported): Promise<void> => {
const localeBranding: Array<IBasicCredentialLocaleBranding> = await Promise.all(
(credentialsConfigSupported.display ?? []).map(
async (display: CredentialsSupportedDisplay): Promise<IBasicCredentialLocaleBranding> =>
await context.agent.ibCredentialLocaleBrandingFrom({ localeBranding: await credentialLocaleBrandingFrom(display) }),
),
)

const defaultCredentialType = 'VerifiableCredential'
const credentialTypes: Array<string> = ('types' in credentialsConfigSupported // TODO credentialsConfigSupported.types is deprecated
? (credentialsConfigSupported.types as string[])
: 'credential_definition' in credentialsConfigSupported
? credentialsConfigSupported.credential_definition.type
: [defaultCredentialType]) ?? [configId]

const filteredCredentialTypes = credentialTypes.filter((type: string): boolean => type !== defaultCredentialType)
credentialBranding[filteredCredentialTypes[0]] = localeBranding // TODO for now taking the first type
},
),
)

return credentialBranding
Expand Down Expand Up @@ -217,13 +219,11 @@ export const mapCredentialToAccept = async (args: MapCredentialToAcceptArgs): Pr
}

export const getDefaultIssuanceOpts = async (args: GetDefaultIssuanceOptsArgs): Promise<IssuanceOpts> => {
//const { credentialSupported, opts, context } = args
const { credentialSupported, context } = args
const { credentialSupported, opts, context } = args

const issuanceOpt = {
...credentialSupported,
didMethod: SupportedDidMethodEnum.DID_JWK,
//didMethod: opts.client.isEBSI() ? SupportedDidMethodEnum.DID_KEY : SupportedDidMethodEnum.DID_JWK, FIXME
didMethod: opts.client.isEBSI() ? SupportedDidMethodEnum.DID_KEY : SupportedDidMethodEnum.DID_JWK,
keyType: 'Secp256r1',
} as IssuanceOpts
const identifierOpts = await getIdentifier({ issuanceOpt, context })
Expand Down Expand Up @@ -365,9 +365,8 @@ export const getIssuanceOpts = async (args: GetIssuanceOptsArgs): Promise<Array<
...credentialSupported,
didMethod,
format: credentialSupported.format,
// keyType: client.isEBSI() ? 'Secp256r1' : keyTypeFromCryptographicSuite({ suite: cryptographicSuite }), FIXME
keyType: keyTypeFromCryptographicSuite({ suite: cryptographicSuite }),
// ...(client.isEBSI() && { codecName: 'EBSI' }), FIXME
keyType: client.isEBSI() ? 'Secp256r1' : keyTypeFromCryptographicSuite({ suite: cryptographicSuite }),
...(client.isEBSI() && { codecName: 'EBSI' }),
} as IssuanceOpts
const identifierOpts = await getIdentifier({ issuanceOpt, context })
if (!client.clientId) {
Expand All @@ -383,7 +382,7 @@ export const getIssuanceOpts = async (args: GetIssuanceOptsArgs): Promise<Array<
}

export const getIssuanceDidMethod = async (opts: GetIssuanceDidMethodArgs): Promise<SupportedDidMethodEnum> => {
const { credentialSupported, didMethodPreferences } = opts
const { client, credentialSupported, didMethodPreferences } = opts
const { format, cryptographic_binding_methods_supported } = credentialSupported
if (cryptographic_binding_methods_supported && Array.isArray(cryptographic_binding_methods_supported)) {
const method: SupportedDidMethodEnum | undefined = didMethodPreferences.find((method: SupportedDidMethodEnum) =>
Expand All @@ -396,11 +395,10 @@ export const getIssuanceDidMethod = async (opts: GetIssuanceDidMethodArgs): Prom
}
}

/*
if (client.isEBSI()) { FIXME
if (client.isEBSI()) {
return SupportedDidMethodEnum.DID_KEY
}
*/

if (!format || (format.includes('jwt') && !format?.includes('jwt_vc_json_ld'))) {
return format ? didMethodPreferences[1] : didMethodPreferences[0]
} else {
Expand All @@ -410,7 +408,7 @@ export const getIssuanceDidMethod = async (opts: GetIssuanceDidMethodArgs): Prom
}

export const getIssuanceCryptoSuite = async (opts: GetIssuanceCryptoSuiteArgs): Promise<string> => {
const { credentialSupported, jwtCryptographicSuitePreferences, jsonldCryptographicSuitePreferences } = opts
const { client, credentialSupported, jwtCryptographicSuitePreferences, jsonldCryptographicSuitePreferences } = opts
const signing_algs_supported: Array<string> = credentialSupported.credential_signing_alg_values_supported ?? []

// TODO: Return array, so the wallet/user could choose
Expand All @@ -425,9 +423,9 @@ export const getIssuanceCryptoSuite = async (opts: GetIssuanceCryptoSuiteArgs):

if (supportedPreferences.length > 0) {
return supportedPreferences[0]
} /*else if (client.isEBSI()) { FIXME
} else if (client.isEBSI()) {
return SignatureAlgorithmEnum.ES256
}*/
}

// if we cannot find supported cryptographic suites, we just try with the first preference
const fallback = jwtCryptographicSuitePreferences[0]
Expand Down
16 changes: 13 additions & 3 deletions packages/oid4vci-holder/src/link-handler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,33 @@ import { convertURIToJsonObject } from '@sphereon/oid4vci-common'
import { DefaultLinkPriorities, LinkHandlerAdapter } from '@sphereon/ssi-sdk.core'
import { IMachineStatePersistence, interpreterStartOrResume } from '@sphereon/ssi-sdk.xstate-machine-persistence'
import { IAgentContext } from '@veramo/core'
import { GetMachineArgs, IOID4VCIHolder, OID4VCIMachineEvents, OID4VCIMachineInterpreter, OID4VCIMachineState } from '../types/IOID4VCIHolder'
import {
GetMachineArgs,
IOID4VCIHolder,
OID4VCIHolderOptions,
OID4VCIMachineEvents,
OID4VCIMachineInterpreter,
OID4VCIMachineState,
} from '../types/IOID4VCIHolder'

export class OID4VCIHolderLinkHandler extends LinkHandlerAdapter {
private readonly context: IAgentContext<IOID4VCIHolder & IMachineStatePersistence>
private readonly stateNavigationListener:
| ((oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState, navigation?: any) => Promise<void>)
| undefined
private noStateMachinePersistence: boolean
private readonly noStateMachinePersistence: boolean
private readonly options?: OID4VCIHolderOptions

constructor(
args: Pick<GetMachineArgs, 'stateNavigationListener'> & {
args: Pick<GetMachineArgs, 'stateNavigationListener' | 'options'> & {
priority?: number | DefaultLinkPriorities
protocols?: Array<string | RegExp>
noStateMachinePersistence?: boolean
context: IAgentContext<IOID4VCIHolder & IMachineStatePersistence>
},
) {
super({ ...args, id: 'OID4VCIHolder' })
this.options = args.options
this.context = args.context
this.noStateMachinePersistence = args.noStateMachinePersistence === true
this.stateNavigationListener = args.stateNavigationListener
Expand All @@ -39,6 +48,7 @@ export class OID4VCIHolderLinkHandler extends LinkHandlerAdapter {
...(hasCode && { code: code }),
uri,
},
options: this.options,
stateNavigationListener: this.stateNavigationListener,
})

Expand Down
15 changes: 13 additions & 2 deletions packages/oid4vci-holder/src/types/IOID4VCIHolder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { OpenID4VCIClient, OpenID4VCIClientState } from '@sphereon/oid4vci-client'
import { AuthorizationResponse, CredentialConfigurationSupported, CredentialResponse, EndpointMetadataResult } from '@sphereon/oid4vci-common'
import {
AuthorizationRequestOpts,
AuthorizationResponse,
CredentialConfigurationSupported,
CredentialResponse,
EndpointMetadataResult,
} from '@sphereon/oid4vci-common'
import { IContactManager } from '@sphereon/ssi-sdk.contact-manager'
import { IBasicCredentialLocaleBranding, IBasicIssuerLocaleBranding, Identity, Party } from '@sphereon/ssi-sdk.data-store'
import { IIssuanceBranding } from '@sphereon/ssi-sdk.issuance-branding'
Expand Down Expand Up @@ -40,6 +46,7 @@ export type OID4VCIHolderOptions = {
onIdentifierCreated?: (args: OnIdentifierCreatedArgs) => Promise<void>
vcFormatPreferences?: Array<string>
jsonldCryptographicSuitePreferences?: Array<string>
defaultAuthRequestOptions?: AuthorizationRequestOpts
didMethodPreferences?: Array<SupportedDidMethodEnum>
jwtCryptographicSuitePreferences?: Array<SignatureAlgorithmEnum>
}
Expand All @@ -60,10 +67,11 @@ export type OnIdentifierCreatedArgs = {

export type GetMachineArgs = {
requestData: RequestData
options?: OID4VCIHolderOptions
stateNavigationListener?: (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState, navigation?: any) => Promise<void>
}

export type InitiateOID4VCIArgs = Pick<OID4VCIMachineContext, 'requestData'>
export type InitiateOID4VCIArgs = Pick<OID4VCIMachineContext, 'requestData' | 'authorizationRequestOpts'>
export type CreateCredentialSelectionArgs = Pick<
OID4VCIMachineContext,
'credentialsSupported' | 'credentialBranding' | 'selectedCredentials' | 'locale' | 'openID4VCIClientState'
Expand Down Expand Up @@ -109,6 +117,7 @@ export type MappedCredentialToAccept = {
}

export type OID4VCIMachineContext = {
authorizationRequestOpts?: AuthorizationRequestOpts
requestData?: RequestData // TODO WAL-673 fix type as this is not always a qr code (deeplink)
locale?: string
authorizationCodeURL?: string
Expand Down Expand Up @@ -194,6 +203,7 @@ export type OID4VCIMachineInstanceOpts = {
guards?: any
subscription?: () => void
requireCustomNavigationHook?: boolean
authorizationRequestOpts?: AuthorizationRequestOpts
stateNavigationListener: (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState, navigation?: any) => Promise<void>
} & CreateOID4VCIMachineOpts

Expand Down Expand Up @@ -317,6 +327,7 @@ export type SelectAppLocaleBrandingArgs = {
}

export type IssuanceOpts = CredentialConfigurationSupported & {
credentialConfigurationId?: string // Explicit ID for a credential
didMethod: SupportedDidMethodEnum
keyType: TKeyType
codecName?: string
Expand Down
6 changes: 3 additions & 3 deletions packages/oid4vci-issuer-rest-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
"start:dev": "ts-node __tests__/RestAPI.ts"
},
"dependencies": {
"@sphereon/oid4vci-common": "0.10.4-unstable.63+1d87208",
"@sphereon/oid4vci-issuer": "0.10.4-unstable.63+1d87208",
"@sphereon/oid4vci-issuer-server": "0.10.4-unstable.63+1d87208",
"@sphereon/oid4vci-common": "0.10.4-unstable.70",
"@sphereon/oid4vci-issuer": "0.10.4-unstable.70",
"@sphereon/oid4vci-issuer-server": "0.10.4-unstable.70",
"@sphereon/ssi-express-support": "workspace:*",
"@sphereon/ssi-sdk.kv-store-temp": "workspace:*",
"@sphereon/ssi-sdk.oid4vci-issuer": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/oid4vci-issuer-rest-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"generate-plugin-schema": "ts-node ../../packages/dev/bin/sphereon.js dev generate-plugin-schema"
},
"dependencies": {
"@sphereon/oid4vci-common": "0.10.4-unstable.63+1d87208",
"@sphereon/oid4vci-common": "0.10.4-unstable.70",
"@sphereon/ssi-types": "workspace:*",
"@veramo/core": "4.2.0",
"cross-fetch": "^3.1.8"
Expand Down
2 changes: 1 addition & 1 deletion packages/oid4vci-issuer-store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"build:clean": "tsc --build --clean && tsc --build"
},
"dependencies": {
"@sphereon/oid4vci-common": "0.10.4-unstable.63+1d87208",
"@sphereon/oid4vci-common": "0.10.4-unstable.70",
"@sphereon/ssi-sdk-ext.did-utils": "0.19.0",
"@sphereon/ssi-sdk.kv-store-temp": "workspace:*",
"@veramo/core": "4.2.0",
Expand Down
Loading

0 comments on commit 434196e

Please sign in to comment.