Skip to content

Commit

Permalink
Task/update issue credential payload (#6)
Browse files Browse the repository at this point in the history
* print fetch error messages
* update: support meeco [email protected]
* update changelog
* add verbose flag to claim
* refactor and cleanup; drop support for org-wallet <0.0.10

---------

Co-authored-by: Fendy Putra <[email protected]>
  • Loading branch information
ragnika and Fendy Putra authored Dec 19, 2023
1 parent 9a278b4 commit 70fb869
Show file tree
Hide file tree
Showing 11 changed files with 134 additions and 41 deletions.
5 changes: 5 additions & 0 deletions .data/basicIdentity.claims.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"given_name": "Ken",
"family_name": "Tanaka",
"birthdate": "1979-02-09"
}
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
## [1.3.2]

add [-v --verbose] flags to `claim` - to print out credential to terminal
Add Support for `organisation-wallet` version `0.0.10`
Drop Support for `organisation-wallet` version `<=0.0.9`

## [1.3.1] (2023.12.18)

Add support to call `claim` and `present` commands with `--url` or `-u` flag to accept url from stdin

## [1.3.0] (2023.12.18)

new format of payload for `/credential` endpoint
Add error messages on failing to fetch credential-offer or claiming credential

## [1.2.1] (2023.12.15)

Bugfix: trim credential-offer and presentation-request input files
Expand Down
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,43 @@
meeco-wallet-cli
=================
# meeco-wallet-cli

<!-- toc -->
* [Configuration](#configuration)
* [Usage](#usage)
* [Commands](#commands)

- [Configuration](#configuration)
- [Usage](#usage)
- [Commands](#commands)
<!-- tocstop -->

<!-- config -->

# Configuration

## Holder Details

```
# config/holder.json
uri: # Holder identifier to be attached to the credential / presentation
jwk: # private secp256r1 Key as JWK
```

<!-- configstop -->

# Usage

<!-- usage -->

```sh-session
$ npm install
$ chmod +x ./bin/dev.js
$ ./bin/dev.js COMMAND
```

<!-- usagestop -->

# Commands

<!-- commands -->

- [meeco-wallet-cli](#meeco-wallet-cli)
- [Configuration](#configuration)
- [Holder Details](#holder-details)
Expand All @@ -46,11 +55,12 @@ Claim Credential Offer

```
USAGE
$ ./bin/dev.js claim [-f <value>]
$ ./bin/dev.js claim [-f <value>] [-v]
FLAGS
-f, --file=<value> credential offer filename in ".data" folder
-u, --url=<value> direct URL for the credential offer
-v, --verbose Print out credential at end of command
DESCRIPTION
Issue a credential by claiming the provided Credential Offer.
Expand Down Expand Up @@ -90,7 +100,8 @@ EXAMPLE FILES
```

# Meeco Organisation Wallet
Compatible version `<=0.0.8`

Compatible version `>=0.0.10`

## `meeco-wallet-cli create-credential-offer`

Expand Down Expand Up @@ -133,4 +144,4 @@ ARGUMENTS
DESCRIPTION
Create a Presentation Request.
Will prompt for a filename to save the created Presentation Request URI.
```
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"prepare": "npm run build",
"version": "oclif readme && git add README.md"
},
"version": "1.3.1",
"version": "1.3.2",
"keywords": [
"oclif",
"sd-jwt",
Expand Down
15 changes: 15 additions & 0 deletions src/commands/claim/claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export default class Claim extends Command {
char: 'u',
description: 'direct URL for the credential offer',
}),
verbose: Flags.boolean({
char: 'v',
description: 'Print out credential at end of command',
})
};

async run(): Promise<void> {
Expand All @@ -49,7 +53,18 @@ export default class Claim extends Command {
}

const verifiableCredential = await claimCredentialOffer(credentialOfferURI);

if (!verifiableCredential) {
return;
}

const vcFilename = await prompt('Save Credential as', { default: prependTS('credential.jwt') });
await writeFile(`${DATA_FOLDER}/${vcFilename}`, verifiableCredential);

if (flags.verbose) {
console.log("==============================");
console.log(verifiableCredential);
console.log("==============================");
}
}
}
12 changes: 10 additions & 2 deletions src/commands/create-credential-offer/create-credential-offer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,20 @@ export default class CreateCredentialOffer extends Command {

const claims = await readFile(claimsFile).then((data) => JSON.parse(data.toString()));

const credentialInformation = selectedCredential.credentialIdentifier
? {
credentialIdentifiers: [selectedCredential.credentialIdentifier]
}
: {
format: selectedCredential.format,
types: selectedCredential.types,
}

const response = await createCredentialOffer({
...credentialInformation,
claims,
format: selectedCredential.format,
grantType: selectedGrantType,
pinRequired,
types: selectedCredential.types,
url: args.url,
}).catch((error) => {
this.log('Failed to Create Credential offer:', error.message);
Expand Down
1 change: 1 addition & 0 deletions src/types/create-credential-offer.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type CredentialMetadata = {
}

export type Credential = {
credentialIdentifier: string;
format: string;
id: string;
name: string;
Expand Down
8 changes: 7 additions & 1 deletion src/types/openid.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@ export type SupportedCredential = {
types?: string[];
}

export type SupportedCredentialMap = {
[identifier: string]: SupportedCredential;
}

export type CredentialOfferDetails = string[];

export type IssuerMetadata = {
[metadata: string]: unknown;
authorization_endpoint?: string; // TODO: remove when no longer supporting org-wallet < 0.0.8
credential_endpoint: string;
credentials_supported: SupportedCredential[];
credentials_supported: SupportedCredential[] | SupportedCredentialMap;
grant_types_supported?: string[]; // TODO: remove when no longer supporting org-wallet < 0.0.8
issuer: string;
pushed_authorization_endpoint?: string;
Expand Down
11 changes: 11 additions & 0 deletions src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ export async function printFetchError(res: Response, message = 'HTTP request fai
console.log(`Response: ${body}`);
}

export async function parseFetchResponse(res: Response) {
if (res.status === 200) {
return res.json();
}

console.log(`==============================================`);
console.log(`Status code: ${res.status}`);
console.log(await res.text());
console.log(`==============================================`);
}

export function isVcSdJwt({ format }: { format: string }) {
return format === JWT_TYPE.VC_SD_JWT;
}
25 changes: 9 additions & 16 deletions src/utils/meeco-org-wallet/credential-offer.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { generateRandomCode } from '../helpers.js';
import { GRANT_TYPES } from '../../types/openid.types.js';
import { generateRandomCode, parseFetchResponse } from '../helpers.js';
import { CREDENTIAL_OFFER_ENDPOINT } from './constants.js';

export type createCredentialOfferArgs = {
claims: unknown;
format: string;
credentialIdentifiers?: string[];
format?: string;
grantType: string;
pinRequired: boolean;
types: string[];
types?: string[];
url: string;
userPin?: string;
}

export async function createCredentialOffer({
claims,
credentialIdentifiers,
format,
grantType,
pinRequired,
Expand All @@ -40,16 +42,13 @@ export async function createCredentialOffer({
};
}

const credentials = credentialIdentifiers ?? [ { format, types } ];

const payload = {
credentialDataSupplierInput: {
claims,
},
credentials: [
{
format,
types,
},
],
credentials,
grants,
}

Expand All @@ -60,11 +59,5 @@ export async function createCredentialOffer({
},
method: 'POST',
})
.then((res) => {
if (res.status !== 200) {
throw new Error(res.statusText);
}

return res.json()
});
.then((res) => parseFetchResponse(res));
}
58 changes: 45 additions & 13 deletions src/utils/openid/vci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { randomUUID } from 'node:crypto';
import { TokenSet } from 'openid-client';

import { Credential, CredentialMetadata } from '../../types/create-credential-offer.types.js';
import { GRANT_TYPES, IssuerMetadata, WELL_KNOWN } from '../../types/openid.types.js';
import { isVcSdJwt, printFetchError } from '../helpers.js';
import { CredentialOfferDetails, GRANT_TYPES, IssuerMetadata, SupportedCredentialMap, WELL_KNOWN } from '../../types/openid.types.js';
import { isVcSdJwt, parseFetchResponse, printFetchError } from '../helpers.js';
import { getOpenidConfiguration } from './openid-config.js';
import { getTokenFromAuthorizationCode } from './vci.auth-code.js';
import { getJwtVcJsonProof, getSdJwtVcJsonProof } from './vci.proof-jwt.js';
Expand All @@ -28,10 +28,17 @@ type CredentialChoice = {
value: Credential;
}

function parseSupportedCredential(credential: CredentialMetadata): CredentialChoice {
class CredentialOfferError extends Error {
constructor(message = "") {
super(message);
}
}

function parseSupportedCredential(credentialIdentifier: string, credential: CredentialMetadata): CredentialChoice {
const credentialTypes = credential.types ?? credential.credential_definition.types ?? [credential.credential_definition.vct as string];

const vc: Credential = {
credentialIdentifier,
format: credential.format,
id: credential.id,
name: credential.display[0].name,
Expand All @@ -48,13 +55,13 @@ function parseSupportedCredential(credential: CredentialMetadata): CredentialCho
export function getCredentialsSupportedAsChoices(metadata: Array<CredentialMetadata> | supportedCredentialMetadata): CredentialChoice[] {

if (Array.isArray(metadata)) {
return metadata.map((credential) => parseSupportedCredential(credential));
return metadata.map((credential) => parseSupportedCredential('', credential));
}

if (typeof metadata === 'object') {
return Object.keys(metadata).map((key) => {
const credentialMetadata = <CredentialMetadata>(metadata as supportedCredentialMetadata)[key];
return parseSupportedCredential(credentialMetadata);
return parseSupportedCredential(key, credentialMetadata);
});
}

Expand All @@ -78,14 +85,15 @@ export async function claimCredentialOffer(credentialOfferURL: string) {
ux.action.stop();

const { credential_issuer: issuer, credentials, grants } = result;
const metadata = credentials[0];

ux.action.start('get issuer metadata');
ux.action.start(`get issuer metadata from ${issuer}`);
const issuerMetadata = await getIssuerMetadata(issuer);
const credentialMetadata = getCredentialInfo(credentials, issuerMetadata);
ux.action.stop();

ux.action.start('get openid config');
const openidConfig = await getOpenidConfiguration(issuer);
const authorizationServer = issuerMetadata.authorization_server ?? issuer;
ux.action.start(`get openid config from ${authorizationServer}`);
const openidConfig = await getOpenidConfiguration(authorizationServer);
ux.action.stop();

const preAuthGrant = grants[GRANT_TYPES.PREAUTHORIZED_CODE];
Expand Down Expand Up @@ -123,7 +131,7 @@ export async function claimCredentialOffer(credentialOfferURL: string) {
throw new Error('could not find a supported grant type');
}

return issueVC(issuer, issuerMetadata.credential_endpoint, token, metadata);
return issueVC(issuer, issuerMetadata.credential_endpoint, token, credentialMetadata);
}

async function exchangePreauthCodeWithToken(endpoint: string, code: string, userPin?: string) {
Expand Down Expand Up @@ -156,8 +164,11 @@ async function exchangePreauthCodeWithToken(endpoint: string, code: string, user
}

type CredentialOfferMetadata = {
credential_definition?: {
vct: string;
};
format: string;
types: string[];
types?: string[]; // TODO: move this types into credential_definition to support jwt_vc_json
}

export async function issueVC(issuer: string, endpoint: string, token: TokenSet, metadata: CredentialOfferMetadata) {
Expand All @@ -182,7 +193,28 @@ export async function issueVC(issuer: string, endpoint: string, token: TokenSet,
'Content-Type': 'application/json',
},
method: 'post',
}).then((res) => res.json());
}).then((res) => parseFetchResponse(res));

return result.credential;
return result?.credential;
}

export function getCredentialInfo(credentials: Array<CredentialOfferDetails>, issuerMetadata: IssuerMetadata): CredentialOfferMetadata {
if (typeof credentials[0] !== 'string') {
throw new CredentialOfferError('Unsupported format of credential_offer.credentials');
}

const credentialIdentifier = credentials[0];
const credentialMetadata = (issuerMetadata.credentials_supported as SupportedCredentialMap)[credentialIdentifier];

return isVcSdJwt(credentialMetadata)
? {
'credential_definition': {
vct: credentialMetadata.credential_definition.vct as string,
},
format: credentialMetadata.format,
}
: {
format: credentialMetadata.format,
types: credentialMetadata.credential_definition?.types ?? credentialMetadata.types
}
}

0 comments on commit 70fb869

Please sign in to comment.