Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: auth code flow #2056

Closed
Closed
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
11 changes: 11 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"presets": [
[
"@babel/preset-env",
{
// "modules": "commonjs",
// "debug": true
}
]
]
}
20 changes: 20 additions & 0 deletions .changeset/shiny-sheep-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@credo-ts/openid4vc': minor
---

feat(openid4vc): oid4vci authorization code flow, presentation during issuance and batch issuance.

This is a big change to OpenID4VCI in Credo, with the neccsary breaking changes since we first added it to the framework. Over time the spec has changed significantly, but also our understanding of the standards and protocols.

**Authorization Code Flow**
Credo now supports the authorization code flow, for both issuer and holder. An issuer can configure multiple authorization servers, and work with external authorization servers as well. The integration is based on OAuth2, with several extension specifications, mainly the OAuth2 JWT Access Token Profile, as well as Token Introspection (for opaque access tokens). Verification works out of the box, as longs as the authorization server has a `jwks_uri` configured. For Token Introspection it's also required to provide a `clientId` and `clientSecret` in the authorization server config.

To use an external authorization server, the authorization server MUST include the `issuer_state` parameter from the credential offer in the access token. Otherwise it's not possible for Credo to correlate the authorization session to the offer session.

The demo-openid contains an example with external authorization server, which can be used as reference. The Credo authorization server supports DPoP and PKCE.

**Batch Issuance**
The credential request to credential mapper has been updated to support multiple proofs, and also multiple credential instances. The client can now also handle batch issuance.

**Presentation During Issuance**
The presenation during issuance allows to request presentation using OID4VP before granting authorization for issuance of one or more credentials. This flow is automatically handled by the `resolveAuthorizationRequest` method on the oid4vci holder service.
20 changes: 15 additions & 5 deletions demo-openid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Alice, a former student of Faber College, connects with the College, is issued a

## Features

- ✅ Issuing a credential.
- ✅ Issuing a credential without authorization (pre authorized).
- ✅ Issuing a credenital with external authorization server
- ✅ Resolving a credential offer.
- ✅ Accepting a credential offer.
- ✅ Requesting a credential presentation.
Expand All @@ -29,7 +30,7 @@ Clone the Credo git repository:
git clone https://github.com/openwallet-foundation/credo-ts.git
```

Open three different terminals next to each other and in both, go to the demo folder:
Open four different terminals next to each other and in each, go to the demo folder:

```sh
cd credo-ts/demo-openid
Expand All @@ -41,13 +42,19 @@ Install the project in one of the terminals:
pnpm install
```

In the first terminal run the Issuer:
In the first terminal run the OpenID Provider:

```sh
pnpm provider
```

In the second terminal run the Issuer:

```sh
pnpm issuer
```

In the second terminal run the Holder:
In the third terminal run the Holder:

```sh
pnpm holder
Expand All @@ -65,7 +72,8 @@ To create a credential offer:

- Go to the Issuer terminal.
- Select `Create a credential offer`.
- Select `UniversityDegreeCredential`.
- Choose whether authorization is required
- Select the credential(s) you want to issue.
- Now copy the content INSIDE the quotes (without the quotes).

To resolve and accept the credential:
Expand All @@ -74,6 +82,8 @@ To resolve and accept the credential:
- Select `Resolve a credential offer`.
- Paste the content copied from the credential offer and hit enter.
- Select `Accept the credential offer`.
- Choose which credential(s) to accept
- If authorization is required a link will be printed in the terminal, open this in your browser. You can sign in using any username and password. Once authenticated return to the terminal
- You have now stored your credential.

To create a presentation request:
Expand Down
10 changes: 8 additions & 2 deletions demo-openid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@
"license": "Apache-2.0",
"scripts": {
"issuer": "ts-node src/IssuerInquirer.ts",
"provider": "tsx src/Provider.ts",
"holder": "ts-node src/HolderInquirer.ts",
"verifier": "ts-node src/VerifierInquirer.ts"
},
"dependencies": {
"@hyperledger/anoncreds-nodejs": "^0.2.2",
"@hyperledger/aries-askar-nodejs": "^0.2.3",
"@hyperledger/indy-vdr-nodejs": "^0.2.2",
"@koa/bodyparser": "^5.1.1",
"express": "^4.18.1",
"inquirer": "^8.2.5"
"inquirer": "^8.2.5",
"jose": "^5.3.0",
"oidc-provider": "^8.4.6"
},
"devDependencies": {
"@credo-ts/askar": "workspace:*",
Expand All @@ -28,8 +32,10 @@
"@types/express": "^4.17.13",
"@types/figlet": "^1.5.4",
"@types/inquirer": "^8.2.6",
"@types/oidc-provider": "^8.4.4",
"clear": "^0.1.0",
"figlet": "^1.5.2",
"ts-node": "^10.9.2"
"ts-node": "^10.4.0",
"tsx": "^4.11.0"
}
}
12 changes: 11 additions & 1 deletion demo-openid/src/BaseAgent.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import type { InitConfig, KeyDidCreateOptions, ModulesMap, VerificationMethod } from '@credo-ts/core'
import type { Express } from 'express'

import { Agent, DidKey, HttpOutboundTransport, KeyType, TypedArrayEncoder } from '@credo-ts/core'
import {
Agent,
ConsoleLogger,
DidKey,
HttpOutboundTransport,
KeyType,
LogLevel,
TypedArrayEncoder,
} from '@credo-ts/core'
import { HttpInboundTransport, agentDependencies } from '@credo-ts/node'
import express from 'express'

Expand All @@ -26,6 +34,8 @@ export class BaseAgent<AgentModules extends ModulesMap> {
const config = {
label: name,
walletConfig: { id: name, key: name },
allowInsecureHttpUrls: true,
logger: new ConsoleLogger(LogLevel.off),
} satisfies InitConfig

this.config = config
Expand Down
86 changes: 52 additions & 34 deletions demo-openid/src/BaseInquirer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,66 @@ export enum ConfirmOptions {
}

export class BaseInquirer {
public optionsInquirer: { type: string; prefix: string; name: string; message: string; choices: string[] }
public inputInquirer: { type: string; prefix: string; name: string; message: string; choices: string[] }

public constructor() {
this.optionsInquirer = {
type: 'list',
prefix: '',
name: 'options',
message: '',
choices: [],
}

this.inputInquirer = {
type: 'input',
prefix: '',
name: 'input',
message: '',
choices: [],
}
private optionsInquirer = {
type: 'list',
prefix: '',
name: 'options',
message: '',
choices: [],
}
private inputInquirer = {
type: 'input',
prefix: '',
name: 'input',
message: '',
choices: [],
}

public async pickOne(options: string[]): Promise<string> {
const result = await prompt([
{
...this.optionsInquirer,
message: Title.OptionsTitle,
choices: options,
},
])

public inquireOptions(promptOptions: string[]) {
this.optionsInquirer.message = Title.OptionsTitle
this.optionsInquirer.choices = promptOptions
return this.optionsInquirer
return result.options
}

public inquireInput(title: string) {
this.inputInquirer.message = title
return this.inputInquirer
public async pickMultiple(options: string[]): Promise<string[]> {
const result = await prompt([
{
...this.optionsInquirer,
message: Title.OptionsTitle,
choices: options,
type: 'checkbox',
},
])

return result.options
}

public inquireConfirmation(title: string) {
this.optionsInquirer.message = title
this.optionsInquirer.choices = [ConfirmOptions.Yes, ConfirmOptions.No]
return this.optionsInquirer
public async inquireInput(title: string): Promise<string> {
const result = await prompt([
{
...this.inputInquirer,
message: title,
},
])

return result.input
}

public async inquireMessage() {
this.inputInquirer.message = Title.MessageTitle
const message = await prompt([this.inputInquirer])
public async inquireConfirmation(title: string) {
const result = await prompt([
{
...this.optionsInquirer,
choices: [ConfirmOptions.Yes, ConfirmOptions.No],
message: title,
},
])

return message.input[0] === 'q' ? null : message.input
return result.options === ConfirmOptions.Yes
}
}
114 changes: 98 additions & 16 deletions demo-openid/src/Holder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ import {
W3cJsonLdVerifiableCredential,
DifPresentationExchangeService,
Mdoc,
DidKey,
DidJwk,
getJwkFromKey,
} from '@credo-ts/core'
import { OpenId4VcHolderModule } from '@credo-ts/openid4vc'
import {
authorizationCodeGrantIdentifier,
OpenId4VcHolderModule,
OpenId4VciAuthorizationFlow,
preAuthorizedCodeGrantIdentifier,
} from '@credo-ts/openid4vc'
import { ariesAskar } from '@hyperledger/aries-askar-nodejs'

import { BaseAgent } from './BaseAgent'
Expand All @@ -28,11 +36,8 @@ export class Holder extends BaseAgent<ReturnType<typeof getOpenIdHolderModules>>
public static async build(): Promise<Holder> {
const holder = new Holder(3000, 'OpenId4VcHolder ' + Math.random().toString())
await holder.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598e')

// Set trusted issuer certificates. Required fro verifying mdoc credentials
const trustedCertificates: string[] = []
await holder.agent.x509.setTrustedCertificates(
trustedCertificates.length === 0 ? undefined : (trustedCertificates as [string, ...string[]])
await holder.agent.x509.addTrustedCertificate(
'MIH7MIGioAMCAQICEFvUcSkwWUaPlEWnrOmu_EYwCgYIKoZIzj0EAwIwDTELMAkGA1UEBhMCREUwIBcNMDAwMTAxMDAwMDAwWhgPMjA1MDAxMDEwMDAwMDBaMA0xCzAJBgNVBAYTAkRFMDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgAC3A9V8ynqRcVjADqlfpZ9X8mwbew0TuQldH_QOpkadsWjAjAAMAoGCCqGSM49BAMCA0gAMEUCIQDXGNookSkHqRXiOP_0fVUdNIScY13h3DWkqSopFIYB2QIgUzNFnZ-SEdm-7UMzggaPiFgtznVzmHw2h4vVtuLzWlA'
)

return holder
Expand All @@ -42,25 +47,102 @@ export class Holder extends BaseAgent<ReturnType<typeof getOpenIdHolderModules>>
return await this.agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer)
}

public async requestAndStoreCredentials(
public async initiateAuthorization(
resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer,
credentialsToRequest: string[]
) {
const tokenResponse = await this.agent.modules.openId4VcHolder.requestToken({ resolvedCredentialOffer })
const grants = resolvedCredentialOffer.credentialOfferPayload.grants
// TODO: extend iniateAuthorization in oid4vci lib? Or not?
if (grants?.[preAuthorizedCodeGrantIdentifier]) {
return {
authorizationFlow: 'PreAuthorized',
preAuthorizedCode: grants[preAuthorizedCodeGrantIdentifier]['pre-authorized_code'],
} as const
} else if (resolvedCredentialOffer.credentialOfferPayload.grants?.[authorizationCodeGrantIdentifier]) {
const resolvedAuthorizationRequest = await this.agent.modules.openId4VcHolder.resolveIssuanceAuthorizationRequest(
resolvedCredentialOffer,
{
clientId: 'foo',
redirectUri: 'http://localhost:3000/redirect',
scope: Object.entries(resolvedCredentialOffer.offeredCredentialConfigurations)
.map(([id, value]) => (credentialsToRequest.includes(id) ? value.scope : undefined))
.filter((v): v is string => Boolean(v)),
}
)

if (resolvedAuthorizationRequest.authorizationFlow === OpenId4VciAuthorizationFlow.PresentationDuringIssuance) {
return {
...resolvedAuthorizationRequest,
authorizationFlow: `${OpenId4VciAuthorizationFlow.PresentationDuringIssuance}`,
} as const
} else {
return {
...resolvedAuthorizationRequest,
authorizationFlow: `${OpenId4VciAuthorizationFlow.Oauth2Redirect}`,
} as const
}
}

throw new Error('Unsupported grant type')
}

public async requestAndStoreCredentials(
resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer,
options: { clientId?: string; codeVerifier?: string; credentialsToRequest: string[]; code?: string }
) {
const tokenResponse = await this.agent.modules.openId4VcHolder.requestToken(
options.code && options.clientId
? {
resolvedCredentialOffer,
clientId: options.clientId,
codeVerifier: options.codeVerifier,
code: options.code,
}
: {
resolvedCredentialOffer,
}
)

const credentialResponse = await this.agent.modules.openId4VcHolder.requestCredentials({
resolvedCredentialOffer,
credentialConfigurationIds: options.credentialsToRequest,
credentialBindingResolver: async ({ keyTypes, supportsJwk, supportedDidMethods, supportsAllDidMethods }) => {
const key = await this.agent.wallet.createKey({
keyType: keyTypes[0],
})

if (supportsAllDidMethods || supportedDidMethods?.includes('did:key')) {
const didKey = new DidKey(key)

return {
method: 'did',
didUrl: `${didKey.did}#${didKey.key.fingerprint}`,
}
}
if (supportedDidMethods?.includes('did:jwk')) {
const didJwk = DidJwk.fromJwk(getJwkFromKey(key))

return {
method: 'did',
didUrl: `${didJwk.did}#0`,
}
}
if (supportsJwk) {
return {
method: 'jwk',
jwk: getJwkFromKey(key),
}
}

throw new Error('unable to determine holder binding')
},
...tokenResponse,
// TODO: add jwk support for holder binding
credentialsToRequest,
credentialBindingResolver: async () => ({
method: 'did',
didUrl: this.verificationMethod.id,
}),
})

const storedCredentials = await Promise.all(
credentialResponse.map((response) => {
const credential = response.credential
credentialResponse.credentials.map((response) => {
// TODO: handle batch issuance
const credential = response.credentials[0]
if (credential instanceof W3cJwtVerifiableCredential || credential instanceof W3cJsonLdVerifiableCredential) {
return this.agent.w3cCredentials.storeCredential({ credential })
} else if (credential instanceof Mdoc) {
Expand Down
Loading
Loading