Skip to content

Commit

Permalink
feat(demo): ngrok and presentation during issuance
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Nov 18, 2024
1 parent 899c89a commit e644178
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 56 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ aries-framework-*.tgz
coverage
.DS_Store
logs.txt
logs/
logs/

ngrok.auth.yml
26 changes: 26 additions & 0 deletions demo-openid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,29 @@ Exit:
Restart:

- Select 'restart', to shutdown the current program and start a new one

### Optional Proxy

By default all services will be started on `localhost`, and thus won't be reachable by other external services (such as a mobile wallet). If you want to expose the required services to the public, you need to expose multiple ngrok tunnels.

We can setup the tunnels automatically using ngrok. First make sure you have an ngrok account and get your access token from this page: https://dashboard.ngrok.com/get-started/setup/

Then copy the `ngrok.auth.example.yml` file to `ngrok.auth.yml`:

```sh
cp ngrok.auth.example.yml ngrok.auth.yml
```

And finally set the `authtoken` to the auth token as displayed in the ngrok dashboard.

Once set up, you can run the following command in a separate terminal window.

```sh
pnpm proxies
```

This will open three proxies. You should then run your demo environments with these proxies:

- `PROVIDER_HOST=https://d404-123-123-123-123.ngrok-free.app ISSUER_HOST=https://d738-123-123-123-123.ngrok-free.app pnpm provider` (ngrok url for port 3042)
- `PROVIDER_HOST=https://d404-123-123-123-123.ngrok-free.app ISSUER_HOST=https://d738-123-123-123-123.ngrok-free.app pnpm issuer` (ngrok url for port 2000)
- `VERIFIER_HOST=https://1d91-123-123-123-123.ngrok-free.app pnpm verifier` (ngrok url for port 4000)
2 changes: 2 additions & 0 deletions demo-openid/ngrok.auth.example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
authtoken: ea8af45e-0a76-44d5-b2a2-bab9d4bfb346
version: '2'
12 changes: 12 additions & 0 deletions demo-openid/ngrok.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: 2

tunnels:
issuer:
proto: http
addr: 2000
provider:
proto: http
addr: 3042
verifier:
proto: http
addr: 4000
3 changes: 2 additions & 1 deletion demo-openid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"issuer": "ts-node src/IssuerInquirer.ts",
"provider": "tsx src/provider.js",
"holder": "ts-node src/HolderInquirer.ts",
"verifier": "ts-node src/VerifierInquirer.ts"
"verifier": "ts-node src/VerifierInquirer.ts",
"proxies": "ngrok --config ngrok.yml,ngrok.auth.yml start provider issuer verifier"
},
"dependencies": {
"@hyperledger/anoncreds-nodejs": "^0.2.2",
Expand Down
8 changes: 4 additions & 4 deletions demo-openid/src/BaseInquirer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,23 @@ export class BaseInquirer {
choices: [],
}

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

return result.options
}

public async pickMultiple(options: string[]): Promise<string[]> {
public async pickMultiple(options: string[], title?: string): Promise<string[]> {
const result = await prompt([
{
...this.optionsInquirer,
message: Title.OptionsTitle,
message: title ?? Title.OptionsTitle,
choices: options,
type: 'checkbox',
},
Expand Down
112 changes: 103 additions & 9 deletions demo-openid/src/Issuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
OpenId4VciSignSdJwtCredentials,
OpenId4VciSignW3cCredentials,
OpenId4VcIssuerRecord,
OpenId4VcVerifierRecord,
} from '@credo-ts/openid4vc'

import { AskarModule } from '@credo-ts/askar'
Expand All @@ -26,14 +27,29 @@ import {
TypedArrayEncoder,
JsonTransformer,
} from '@credo-ts/core'
import { OpenId4VcIssuerModule, OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc'
import {
OpenId4VcIssuerModule,
OpenId4VcVerifierApi,
OpenId4VcVerifierModule,
OpenId4VciCredentialFormatProfile,
} from '@credo-ts/openid4vc'
import { ariesAskar } from '@hyperledger/aries-askar-nodejs'
import { Router } from 'express'

import { BaseAgent } from './BaseAgent'
import { Output } from './OutputClass'

const PROVIDER_HOST = process.env.PROVIDER_HOST ?? 'http://localhost:3042'
const ISSUER_HOST = process.env.ISSUER_HOST ?? 'http://localhost:2000'

export const credentialConfigurationsSupported = {
PresentationAuthorization: {
format: OpenId4VciCredentialFormatProfile.SdJwtVc,
vct: 'PresentationAuthorization',
scope: 'openid4vc:credential:PresentationAuthorization',
cryptographic_binding_methods_supported: ['jwk', 'did:key', 'did:jwk'],
credential_signing_alg_values_supported: ['ES256', 'EdDSA'],
},
'UniversityDegreeCredential-jwtvcjson': {
format: OpenId4VciCredentialFormatProfile.JwtVcJson,
scope: 'openid4vc:credential:UniversityDegreeCredential-jwtvcjson',
Expand Down Expand Up @@ -81,6 +97,27 @@ function getCredentialRequestToCredentialMapper({
const credentialConfigurationId = credentialConfigurationIds[0]
const credentialConfiguration = supported[credentialConfigurationId]

if (credentialConfigurationId === 'PresentationAuthorization') {
return {
credentialConfigurationId,
format: ClaimFormat.SdJwtVc,
credentials: holderBindings.map((holderBinding) => ({
payload: {
vct: credentialConfiguration.vct,
authorized_user: authorization.accessToken.payload.sub,
},
holder: holderBinding,
issuer:
holderBindings[0].method === 'did'
? {
method: 'did',
didUrl: `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}`,
}
: { method: 'x5c', x5c: [trustedCertificates[0]], issuer: ISSUER_HOST },
})),
} satisfies OpenId4VciSignSdJwtCredentials
}

if (credentialConfiguration.format === OpenId4VciCredentialFormatProfile.JwtVcJson) {
holderBindings.forEach((holderBinding) => assertDidBasedHolderBinding(holderBinding))

Expand Down Expand Up @@ -156,31 +193,78 @@ function getCredentialRequestToCredentialMapper({
export class Issuer extends BaseAgent<{
askar: AskarModule
openId4VcIssuer: OpenId4VcIssuerModule
openId4VcVerifier: OpenId4VcVerifierModule
}> {
public issuerRecord!: OpenId4VcIssuerRecord
public verifierRecord!: OpenId4VcVerifierRecord

public constructor(port: number, name: string) {
public constructor(url: string, port: number, name: string) {
const openId4VciRouter = Router()
const openId4VpRouter = Router()

super({
port,
name,
modules: {
askar: new AskarModule({ ariesAskar }),
openId4VcVerifier: new OpenId4VcVerifierModule({
baseUrl: `${url}/oid4vp`,
router: openId4VpRouter,
}),
openId4VcIssuer: new OpenId4VcIssuerModule({
baseUrl: 'http://localhost:2000/oid4vci',
baseUrl: `${url}/oid4vci`,
router: openId4VciRouter,
credentialRequestToCredentialMapper: (...args) =>
getCredentialRequestToCredentialMapper({ issuerDidKey: this.didKey })(...args),
getVerificationSessionForIssuanceSessionAuthorization: async ({ agentContext, scopes }) => {
const verifierApi = agentContext.dependencyManager.resolve(OpenId4VcVerifierApi)
const authorizationRequest = await verifierApi.createAuthorizationRequest({
verifierId: this.verifierRecord.verifierId,
requestSigner: {
method: 'did',
didUrl: `${this.didKey.did}#${this.didKey.key.fingerprint}`,
},
responseMode: 'direct_post.jwt',
presentationExchange: {
definition: {
id: '18e2c9c3-1722-4393-a558-f0ce1e32c4ec',
input_descriptors: [
{
id: '16f00df5-67f1-47e6-81b1-bd3e3743f84c',
constraints: {
fields: [
{
path: ['$.vct'],
filter: {
type: 'string',
const: credentialConfigurationsSupported.PresentationAuthorization.vct,
},
},
],
},
},
],
name: 'Presentation Authorization',
purpose: `To issue the requested credentials, we need to verify your 'Presentation Authorization' credential`,
},
},
})

return {
scopes,
...authorizationRequest,
}
},
}),
},
})

this.app.use('/oid4vci', openId4VciRouter)
this.app.use('/oid4vp', openId4VpRouter)
}

public static async build(): Promise<Issuer> {
const issuer = new Issuer(2000, 'OpenId4VcIssuer ' + Math.random().toString())
const issuer = new Issuer(ISSUER_HOST, 2000, 'OpenId4VcIssuer ' + Math.random().toString())
await issuer.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598f')

const selfSignedCertificate = await X509Service.createSelfSignedCertificate(issuer.agent.context, {
Expand All @@ -190,7 +274,7 @@ export class Issuer extends BaseAgent<{
}),
notBefore: new Date('2000-01-01'),
notAfter: new Date('2050-01-01'),
extensions: [],
extensions: [[{ type: 'dns', value: ISSUER_HOST.replace('https://', '').replace('http://', '') }]],
name: 'C=DE',
})

Expand All @@ -199,12 +283,15 @@ export class Issuer extends BaseAgent<{
console.log('Set the following certficate for the holder to verify mdoc credentials.')
console.log(issuerCertficicate)

issuer.verifierRecord = await issuer.agent.modules.openId4VcVerifier.createVerifier({
verifierId: '726222ad-7624-4f12-b15b-e08aa7042ffa',
})
issuer.issuerRecord = await issuer.agent.modules.openId4VcIssuer.createIssuer({
issuerId: '726222ad-7624-4f12-b15b-e08aa7042ffa',
credentialConfigurationsSupported,
authorizationServerConfigs: [
{
issuer: 'http://localhost:3042',
issuer: PROVIDER_HOST,
clientAuthentication: {
clientId: 'issuer-server',
clientSecret: 'issuer-server',
Expand All @@ -221,7 +308,7 @@ export class Issuer extends BaseAgent<{

public async createCredentialOffer(options: {
credentialConfigurationIds: string[]
requireAuthorization: boolean
requireAuthorization?: 'presentation' | 'browser'
requirePin: boolean
}) {
const issuerMetadata = await this.agent.modules.openId4VcIssuer.getIssuerMetadata(this.issuerRecord.issuerId)
Expand All @@ -233,15 +320,22 @@ export class Issuer extends BaseAgent<{
preAuthorizedCodeFlowConfig: !options.requireAuthorization
? {
authorizationServerUrl: issuerMetadata.credentialIssuer.credential_issuer,
txCode: options.requirePin ? {} : undefined,
txCode: options.requirePin
? {
input_mode: 'numeric',
length: 4,
description: 'Pin has been printed to the terminal',
}
: undefined,
}
: undefined,
// Auth using external authorization server
authorizationCodeFlowConfig: options.requireAuthorization
? {
authorizationServerUrl: 'http://localhost:3042',
authorizationServerUrl: options.requireAuthorization === 'browser' ? PROVIDER_HOST : undefined,
// TODO: should be generated by us, if we're going to use for matching
issuerState: utils.uuid(),
requirePresentationDuringIssuance: options.requireAuthorization === 'presentation',
}
: undefined,
})
Expand Down
24 changes: 18 additions & 6 deletions demo-openid/src/IssuerInquirer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { textSync } from 'figlet'

import { BaseInquirer } from './BaseInquirer'
import { credentialConfigurationsSupported, Issuer } from './Issuer'
import { Title, greenText, purpleText } from './OutputClass'
import { Title, greenText, purpleText, redText } from './OutputClass'

export const runIssuer = async () => {
clear()
Expand Down Expand Up @@ -49,13 +49,25 @@ export class IssuerInquirer extends BaseInquirer {
}

public async createCredentialOffer() {
const credentialConfigurationIds = await this.pickMultiple(Object.keys(credentialConfigurationsSupported))
const requireAuthorization = await this.inquireConfirmation('Require authorization?')
const requirePin = !requireAuthorization ? await this.inquireConfirmation('Require pin?') : false
let credentialConfigurationIds = await this.pickMultiple(Object.keys(credentialConfigurationsSupported))
while (credentialConfigurationIds.length === 0) {
console.log(redText('Pick at least one', true))
credentialConfigurationIds = await this.pickMultiple(Object.keys(credentialConfigurationsSupported))
}

const authorizationMethod = await this.pickOne(
['Transaction Code', 'Browser', 'Presentation', 'None'],
'Authorization method'
)
const { credentialOffer, issuanceSession } = await this.issuer.createCredentialOffer({
credentialConfigurationIds,
requireAuthorization,
requirePin,
requireAuthorization:
authorizationMethod === 'Browser'
? 'browser'
: authorizationMethod === 'Presentation'
? 'presentation'
: undefined,
requirePin: authorizationMethod === 'Transaction Code',
})

console.log(purpleText(`credential offer: '${credentialOffer}'`, true))
Expand Down
13 changes: 9 additions & 4 deletions demo-openid/src/Provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { Provider } from 'oidc-provider'
// and only works if only person is authenticating. Of course very unsecure, but it's a demo
let issuer_state: string | undefined = undefined

const oidc = new Provider('http://localhost:3042', {
const PROVIDER_HOST = process.env.PROVIDER_HOST ?? 'http://localhost:3042'
const ISSUER_HOST = process.env.ISSUER_HOST ?? 'http://localhost:2000'

const oidc = new Provider(PROVIDER_HOST, {
clientAuthMethods: ['client_secret_basic', 'client_secret_post', 'none'],
clients: [
{
Expand All @@ -15,6 +18,7 @@ const oidc = new Provider('http://localhost:3042', {
grant_types: ['authorization_code'],
id_token_signed_response_alg: 'ES256',
redirect_uris: [],
application_type: 'native',
},
{
client_id: 'issuer-server',
Expand Down Expand Up @@ -71,7 +75,7 @@ const oidc = new Provider('http://localhost:3042', {
enabled: true,
},
resourceIndicators: {
defaultResource: () => 'http://localhost:2000/oid4vci/726222ad-7624-4f12-b15b-e08aa7042ffa',
defaultResource: () => `${ISSUER_HOST}/oid4vci/726222ad-7624-4f12-b15b-e08aa7042ffa`,
enabled: true,
getResourceServerInfo: (context) => {
return {
Expand All @@ -80,7 +84,7 @@ const oidc = new Provider('http://localhost:3042', {

// NOTE: switch this between opaque and jwt to use JWT tokens or Token introspection
accessTokenFormat: 'jwt',
audience: 'http://localhost:2000/oid4vci/726222ad-7624-4f12-b15b-e08aa7042ffa',
audience: `${ISSUER_HOST}/oid4vci/726222ad-7624-4f12-b15b-e08aa7042ffa`,
jwt: {
sign: {
kid: 'first-key',
Expand All @@ -104,6 +108,7 @@ const oidc = new Provider('http://localhost:3042', {
}
},
})
oidc.proxy = true

oidc.use(bodyParser())
oidc.use(async (ctx, next) => {
Expand Down Expand Up @@ -135,5 +140,5 @@ oidc.use(async (ctx, next) => {
})

oidc.listen(3042, () => {
console.log('oidc-provider listening on port 3042, check http://localhost:3042/.well-known/openid-configuration')
console.log(`oidc-provider listening on port 3042, check ${PROVIDER_HOST}/.well-known/openid-configuration`)
})
Loading

0 comments on commit e644178

Please sign in to comment.