Skip to content

Commit

Permalink
feat: working and tests passing
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Nov 8, 2024
1 parent 9e1050b commit c7f9eb3
Show file tree
Hide file tree
Showing 75 changed files with 2,910 additions and 3,437 deletions.
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
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
}
}
122 changes: 94 additions & 28 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,41 +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 resolvedAuthorizationRequest = await this.agent.modules.openId4VcHolder.resolveIssuanceAuthorizationRequest(
resolvedCredentialOffer,
{
clientId: 'foo',
redirectUri: 'http://example.com',
scope: ['openid', 'UniversityDegree'],
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
}
)
}

let code = 'not a valid code!'
code = 'MU_MtTZjjhjmzuzGZdsLSam2GcC-7c4g_k5ukV2XO3i'
throw new Error('Unsupported grant type')
}

const tokenResponse = await this.agent.modules.openId4VcHolder.requestToken({
resolvedAuthorizationRequest,
code,
resolvedCredentialOffer,
})
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,
credentialsToRequest,
credentialBindingResolver: async () => ({
method: 'did',
didUrl: this.verificationMethod.id,
}),
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,
})

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

0 comments on commit c7f9eb3

Please sign in to comment.