Skip to content

Commit

Permalink
feat: integrate dpop functionality
Browse files Browse the repository at this point in the history
Signed-off-by: Martin Auer <martin.auer97@gmail.com>
  • Loading branch information
auer-martin committed Jul 25, 2024
1 parent 88e2ee5 commit 8765c0b
Show file tree
Hide file tree
Showing 17 changed files with 252 additions and 202 deletions.
4 changes: 4 additions & 0 deletions packages/core/src/modules/x509/X509ModuleConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export interface X509ModuleConfigOptions {
/**
*
* Array of trusted base64-encoded certificate strings in the DER-format.
*/
trustedCertificates?: [string, ...string[]]
}

Expand Down
8 changes: 4 additions & 4 deletions packages/openid4vc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
},
"dependencies": {
"@credo-ts/core": "workspace:*",
"@sphereon/did-auth-siop": "0.15.1-next.4",
"@sphereon/oid4vci-client": "0.15.1-next.4",
"@sphereon/oid4vci-common": "0.15.1-next.4",
"@sphereon/oid4vci-issuer": "0.15.1-next.4",
"@sphereon/did-auth-siop": "link:../../../../Documents/OID4VCI/packages/siop-oid4vp",
"@sphereon/oid4vci-client": "link:../../../../Documents/OID4VCI/packages/client",
"@sphereon/oid4vci-common": "link:../../../../Documents/OID4VCI/packages/common",
"@sphereon/oid4vci-issuer": "link:../../../../Documents/OID4VCI/packages/issuer",
"@sphereon/ssi-types": "0.26.1-next.132",
"class-transformer": "^0.5.1",
"rxjs": "^7.8.0"
Expand Down
14 changes: 8 additions & 6 deletions packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,12 @@ export class OpenId4VcHolderApi {
* @param options.code The authorization code obtained via the authorization request URI
*/
public async requestToken(options: OpenId4VciRequestTokenOptions): Promise<OpenId4VciRequestTokenResponse> {
const { access_token: accessToken, c_nonce: cNonce } = await this.openId4VciHolderService.requestAccessToken(
this.agentContext,
options
)
return { accessToken, cNonce }
const {
access_token: accessToken,
c_nonce: cNonce,
dPoPJwk,
} = await this.openId4VciHolderService.requestAccessToken(this.agentContext, options)
return { accessToken, cNonce, dPoPJwk }
}

/**
Expand All @@ -160,13 +161,14 @@ export class OpenId4VcHolderApi {
* @param options.tokenResponse Obtained through @see requestAccessToken
*/
public async requestCredentials(options: OpenId4VciRequestCredentialOptions) {
const { resolvedCredentialOffer, cNonce, accessToken, ...credentialRequestOptions } = options
const { resolvedCredentialOffer, cNonce, accessToken, dPoPJwk, ...credentialRequestOptions } = options

return this.openId4VciHolderService.acceptCredentialOffer(this.agentContext, {
resolvedCredentialOffer,
acceptCredentialOfferOptions: credentialRequestOptions,
accessToken,
cNonce,
dPoPJwk,
})
}

Expand Down
104 changes: 88 additions & 16 deletions packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,13 @@ import type {
OpenId4VciCredentialSupported,
OpenId4VciIssuerMetadata,
} from '../shared'
import type { AgentContext, JwaSignatureAlgorithm, Key, JwkJson } from '@credo-ts/core'
import type {
AccessTokenResponse,
CredentialResponse,
Jwt,
OpenIDResponse,
AuthorizationDetails,
AuthorizationDetailsJwtVcJson,
CredentialIssuerMetadataV1_0_11,
CredentialIssuerMetadataV1_0_13,
} from '@sphereon/oid4vci-common'

import {
AgentContext,
JwaSignatureAlgorithm,
Key,
JwkJson,
Jwk,
SdJwtVcApi,
getJwkFromJson,
DidsApi,
Expand Down Expand Up @@ -60,7 +54,23 @@ import {
OpenID4VCIClientV1_0_11,
OpenID4VCIClientV1_0_13,
} from '@sphereon/oid4vci-client'

Check failure on line 56 in packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts

View workflow job for this annotation

GitHub Actions / Validate

Unable to resolve path to module '@sphereon/oid4vci-client'
import { CodeChallengeMethod, OpenId4VCIVersion, PARMode, post } from '@sphereon/oid4vci-common'
import {
AccessTokenResponse,
CredentialResponse,
Jwt,
OpenIDResponse,
AuthorizationDetails,
AuthorizationDetailsJwtVcJson,
CredentialIssuerMetadataV1_0_11,
CredentialIssuerMetadataV1_0_13,
CodeChallengeMethod,
OpenId4VCIVersion,
PARMode,
post,
SigningAlgo,
EndpointMetadataResult,
CreateDPoPClientOptions,
} from '@sphereon/oid4vci-common'

Check failure on line 73 in packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts

View workflow job for this annotation

GitHub Actions / Validate

Unable to resolve path to module '@sphereon/oid4vci-common'

import { OpenId4VciCredentialFormatProfile } from '../shared'
import {
Expand All @@ -69,7 +79,7 @@ import {
credentialsSupportedV11ToV13,
} from '../shared/issuerMetadataUtils'
import { OpenId4VciCredentialSupportedWithId } from '../shared/models/index'
import { getSupportedJwaSignatureAlgorithms, isCredentialOfferV1Draft13 } from '../shared/utils'
import { getCreateJwtCallback, getSupportedJwaSignatureAlgorithms, isCredentialOfferV1Draft13 } from '../shared/utils'

import { openId4VciSupportedCredentialFormats, OpenId4VciNotificationMetadata } from './OpenId4VciHolderServiceOptions'

Expand Down Expand Up @@ -263,6 +273,44 @@ export class OpenId4VciHolderService {
}
}

private async getCreateDPoPOptions(
agentContext: AgentContext,
metadata: Pick<EndpointMetadataResult, 'authorizationServerMetadata'> & {
credentialIssuerMetadata: OpenId4VciIssuerMetadata
}
) {
const dPoPSigningAlgValuesSupported =
metadata.authorizationServerMetadata?.dpop_signing_alg_values_supported ??
metadata.credentialIssuerMetadata.dpop_signing_alg_values_supported

if (!dPoPSigningAlgValuesSupported) return undefined

const alg = dPoPSigningAlgValuesSupported
.flatMap((alg) => alg)
.find((alg) => getJwkClassFromJwaSignatureAlgorithm(alg))

const JwkClass = alg ? getJwkClassFromJwaSignatureAlgorithm(alg) : undefined

if (!JwkClass) {
throw new CredoError(
`No supported dpop signature algorithms found in dpop_signing_alg_values_supported '${dPoPSigningAlgValuesSupported.join(
', '
)}'`
)
}

const key = await agentContext.wallet.createKey({ keyType: JwkClass.keyType })
const jwk = getJwkFromKey(key)

const createDPoPOptions: CreateDPoPClientOptions = {
jwtIssuer: { alg: alg as unknown as SigningAlgo, jwk: jwk.toJson() },
dPoPSigningAlgValuesSupported,
jwtPayloadProps: {},
createJwtCallback: getCreateJwtCallback(agentContext),
}
return createDPoPOptions
}

public async requestAccessToken(agentContext: AgentContext, options: OpenId4VciTokenRequestOptions) {
const { resolvedCredentialOffer, txCode, resolvedAuthorizationRequest, code } = options
const { metadata, credentialOfferRequestWithBaseUrl } = resolvedCredentialOffer
Expand All @@ -271,6 +319,11 @@ export class OpenId4VciHolderService {
let accessTokenResponse: OpenIDResponse<AccessTokenResponse>

const accessTokenClient = new AccessTokenClient()

const createDPoPOptions = await this.getCreateDPoPOptions(agentContext, metadata)
const dPoPJwk = createDPoPOptions ? getJwkFromJson(createDPoPOptions.jwtIssuer.jwk) : undefined

resolvedCredentialOffer.metadata.credentialIssuerMetadata
if (resolvedAuthorizationRequest) {
const { codeVerifier, redirectUri } = resolvedAuthorizationRequest
accessTokenResponse = await accessTokenClient.acquireAccessToken({
Expand All @@ -280,12 +333,14 @@ export class OpenId4VciHolderService {
code,
codeVerifier,
redirectUri,
createDPoPOptions,
})
} else {
accessTokenResponse = await accessTokenClient.acquireAccessToken({
metadata: metadata,
credentialOffer: { credential_offer: credentialOfferRequestWithBaseUrl.credential_offer },
pin: txCode,
createDPoPOptions,
})
}

Expand All @@ -297,7 +352,7 @@ export class OpenId4VciHolderService {

this.logger.debug('Requested OpenId4VCI Access Token.')

return accessTokenResponse.successBody
return { ...accessTokenResponse.successBody, dPoPJwk }
}

public async acceptCredentialOffer(
Expand All @@ -308,6 +363,7 @@ export class OpenId4VciHolderService {
resolvedAuthorizationRequestWithCode?: OpenId4VciResolvedAuthorizationRequestWithCode
accessToken?: string
cNonce?: string
dPoPJwk?: Jwk
}
) {
const { resolvedCredentialOffer, acceptCredentialOfferOptions } = options
Expand All @@ -320,7 +376,9 @@ export class OpenId4VciHolderService {
return []
}

this.logger.info(`Accepting the following credential offers '${credentialsToRequest}'`)
this.logger.info(
`Accepting the following credential offers '${credentialsToRequest ? credentialsToRequest.join(', ') : 'all'}`
)

const supportedJwaSignatureAlgorithms = getSupportedJwaSignatureAlgorithms(agentContext)

Expand All @@ -347,7 +405,7 @@ export class OpenId4VciHolderService {
} as OpenId4VciTokenRequestOptions

const tokenResponse = options.accessToken
? { access_token: options.accessToken, c_nonce: options.cNonce }
? { access_token: options.accessToken, c_nonce: options.cNonce, dPoPJwk: options.dPoPJwk }
: await this.requestAccessToken(agentContext, tokenRequestOptions)

const receivedCredentials: Array<OpenId4VciCredentialResponse> = []
Expand Down Expand Up @@ -412,10 +470,24 @@ export class OpenId4VciHolderService {
.withToken(tokenResponse.access_token)

const credentialRequestClient = credentialRequestBuilder.build()

let createDPoPOptions: CreateDPoPClientOptions | undefined
if (options.dPoPJwk) {
const jwk = options.dPoPJwk
const alg = jwk.supportedSignatureAlgorithms[0]

createDPoPOptions = {
jwtIssuer: { alg: alg as unknown as SigningAlgo, jwk: jwk.toJson() },
jwtPayloadProps: { accessToken: options.accessToken },
createJwtCallback: getCreateJwtCallback(agentContext),
}
}

const credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({
proofInput: proofOfPossession,
credentialTypes: getTypesFromCredentialSupported(offeredCredentialConfiguration),
format: offeredCredentialConfiguration.format,
createDPoPOptions,
})

newCNonce = credentialResponse.successBody?.c_nonce
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
OpenId4VciIssuerMetadata,
OpenId4VciCredentialOfferPayload,
} from '../shared'
import type { JwaSignatureAlgorithm, KeyType } from '@credo-ts/core'
import type { JwaSignatureAlgorithm, Jwk, KeyType } from '@credo-ts/core'
import type { VerifiableCredential } from '@credo-ts/core/src/modules/dif-presentation-exchange/models/index'
import type {
AccessTokenResponse,
Expand Down Expand Up @@ -42,7 +42,7 @@ export type OpenId4VciNotificationEvent = 'credential_accepted' | 'credential_fa

export type OpenId4VciTokenResponse = Pick<AccessTokenResponse, 'access_token' | 'c_nonce'>

export type OpenId4VciRequestTokenResponse = { accessToken: string; cNonce?: string }
export type OpenId4VciRequestTokenResponse = { accessToken: string; cNonce?: string; dPoPJwk?: Jwk }

export interface OpenId4VciCredentialResponse {
credential: VerifiableCredential
Expand Down Expand Up @@ -112,6 +112,7 @@ export interface OpenId4VciCredentialRequestOptions extends Omit<OpenId4VciAccep
resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer
accessToken: string
cNonce?: string
dPoPJwk?: Jwk
}
/**
* Options that are used to accept a credential offer for both the pre-authorized code flow and authorization code flow.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class OpenId4VcIssuerApi {
}

public async updateIssuerMetadata(
options: Pick<OpenId4VcIssuerRecordProps, 'issuerId' | 'display'> &
options: Pick<OpenId4VcIssuerRecordProps, 'issuerId' | 'display' | 'dPoPSigningAlgValuesSupported'> &
(OpenId4VcIssuerRecordCredentialSupportedProps | OpenId4VcIssuerRecordCredentialConfigurationsSupportedProps)
) {
const issuer = await this.openId4VcIssuerService.getIssuerByIssuerId(this.agentContext, options.issuerId)
Expand All @@ -76,6 +76,7 @@ export class OpenId4VcIssuerApi {
issuer.credentialConfigurationsSupported = undefined
}
issuer.display = options.display
issuer.dPoPSigningAlgValuesSupported = options.dPoPSigningAlgValuesSupported

return this.openId4VcIssuerService.updateIssuer(this.agentContext, issuer)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ export class OpenId4VcIssuerService {
const openId4VcIssuerBase = {
issuerId: options.issuerId ?? utils.uuid(),
display: options.display,
dPoPSigningAlgValuesSupported: options.dPoPSigningAlgValuesSupported,
accessTokenPublicKeyFingerprint: accessTokenSignerKey.fingerprint,
} as const

Expand Down Expand Up @@ -344,6 +345,7 @@ export class OpenId4VcIssuerService {
issuerRecord.credentialConfigurationsSupported ??
credentialsSupportedV11ToV13(agentContext, issuerRecord.credentialsSupported),
issuerDisplay: issuerRecord.display,
dPoPSigningAlgValuesSupported: issuerRecord.dPoPSigningAlgValuesSupported,
} satisfies OpenId4VcIssuerMetadata

return issuerMetadata
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import type {
OpenId4VciIssuerMetadataDisplay,
OpenId4VciTxCode,
} from '../shared'
import type { AgentContext, ClaimFormat, W3cCredential, SdJwtVcSignOptions } from '@credo-ts/core'
import type {
AgentContext,
ClaimFormat,
W3cCredential,
SdJwtVcSignOptions,
JwaSignatureAlgorithm,
} from '@credo-ts/core'

export interface OpenId4VciPreAuthorizedCodeFlowConfig {
preAuthorizedCode?: string
Expand All @@ -39,6 +45,7 @@ export type OpenId4VcIssuerMetadata = {
issuerDisplay?: OpenId4VciIssuerMetadataDisplay[]
credentialsSupported: OpenId4VciCredentialSupportedWithId[]
credentialConfigurationsSupported: OpenId4VciCredentialConfigurationsSupported
dPoPSigningAlgValuesSupported?: [JwaSignatureAlgorithm, ...JwaSignatureAlgorithm[]]
}

export interface OpenId4VciCreateCredentialOfferOptions {
Expand Down Expand Up @@ -152,4 +159,5 @@ export type OpenId4VciCreateIssuerOptions = {
issuerId?: string

display?: OpenId4VciIssuerMetadataDisplay[]
dPoPSigningAlgValuesSupported?: [JwaSignatureAlgorithm, ...JwaSignatureAlgorithm[]]
} & (OpenId4VcIssuerRecordCredentialSupportedProps | OpenId4VcIssuerRecordCredentialConfigurationsSupportedProps)
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {
OpenId4VciCredentialConfigurationsSupported,
OpenId4VciIssuerMetadataDisplay,
} from '../../shared'
import type { RecordTags, TagsBase } from '@credo-ts/core'
import type { JwaSignatureAlgorithm, RecordTags, TagsBase } from '@credo-ts/core'

import { BaseRecord, utils } from '@credo-ts/core'

Expand Down Expand Up @@ -38,6 +38,12 @@ export type OpenId4VcIssuerRecordProps = {
*/
accessTokenPublicKeyFingerprint: string

/**
* The DPoP signing algorithms supported by this issuer.
* If not provided, dPoP is considered unsupported.
*/
dPoPSigningAlgValuesSupported?: [JwaSignatureAlgorithm, ...JwaSignatureAlgorithm[]]

display?: OpenId4VciIssuerMetadataDisplay[]
} & (OpenId4VcIssuerRecordCredentialSupportedProps | OpenId4VcIssuerRecordCredentialConfigurationsSupportedProps)

Expand All @@ -56,6 +62,7 @@ export class OpenId4VcIssuerRecord extends BaseRecord<DefaultOpenId4VcIssuerReco
public credentialsSupported!: OpenId4VciCredentialSupportedWithId[]
public credentialConfigurationsSupported?: OpenId4VciCredentialConfigurationsSupported
public display?: OpenId4VciIssuerMetadataDisplay[]
public dPoPSigningAlgValuesSupported?: [JwaSignatureAlgorithm, ...JwaSignatureAlgorithm[]]

public constructor(props: OpenId4VcIssuerRecordProps) {
super()
Expand All @@ -70,6 +77,7 @@ export class OpenId4VcIssuerRecord extends BaseRecord<DefaultOpenId4VcIssuerReco
this.credentialsSupported =
props.credentialsSupported ?? credentialsSupportedV13ToV11(props.credentialConfigurationsSupported)
this.credentialConfigurationsSupported = props.credentialConfigurationsSupported
this.dPoPSigningAlgValuesSupported = props.dPoPSigningAlgValuesSupported
this.display = props.display
}
}
Expand Down
Loading

0 comments on commit 8765c0b

Please sign in to comment.