Skip to content

Commit

Permalink
feat: added VcIssuer and builders related to that
Browse files Browse the repository at this point in the history
  • Loading branch information
sksadjad committed Apr 3, 2023
1 parent 76536ac commit c2592a8
Show file tree
Hide file tree
Showing 19 changed files with 702 additions and 214 deletions.
4 changes: 2 additions & 2 deletions packages/client/lib/CredentialRequestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { CredentialRequest, CredentialResponse, OpenIDResponse, ProofOfPossessio
import { CredentialFormat } from '@sphereon/ssi-types';
import Debug from 'debug';

import {CredentialRequestClientBuilder} from "./CredentialRequestClientBuilder";
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
import { isValidURL, post } from './functions';
import {CredentialRequestClientBuilder} from "./CredentialRequestClientBuilder";


const debug = Debug('sphereon:openid4vci:credential');
Expand Down Expand Up @@ -71,7 +71,7 @@ export class CredentialRequestClient {
'proof_type' in proofInput ? await ProofOfPossessionBuilder.fromProof(proofInput as ProofOfPossession).build() : await proofInput.build();
return {
type: credentialType ? credentialType : this.credentialRequestOpts.credentialType,
format: format ? format : this.credentialRequestOpts.format,
format: format ? format as string : this.credentialRequestOpts.format as string,
proof,
};
}
Expand Down
83 changes: 83 additions & 0 deletions packages/common/lib/functions/Encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { parse } from 'querystring';

import jwt_decode from 'jwt-decode';

import { BAD_PARAMS, JWTHeader } from '../index';

export function getKidFromJWT(jwt: string): string {
const header: JWTHeader = jwt_decode(jwt);
return header.kid as string;
}

export function decodeUriAsJson(uri: string) {
if (!uri) {
throw new Error(BAD_PARAMS);
}
const queryString = uri.replace(/^([a-zA-Z-_]+:\/\/[?]?)/g, '');
if (!queryString) {
throw new Error(BAD_PARAMS);
}
const parts = parse(queryString);

const json = {};
for (const key in parts) {
const value = parts[key];
if (!value) {
continue;
}
const isBool = typeof value === 'boolean';
const isNumber = typeof value === 'number';
const isString = typeof value == 'string';

if (isBool || isNumber) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
json[decodeURIComponent(key)] = value;
} else if (isString) {
const decoded = decodeURIComponent(value);
if (decoded.startsWith('{') && decoded.endsWith('}')) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
json[decodeURIComponent(key)] = JSON.parse(decoded);
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
json[decodeURIComponent(key)] = decoded;
}
}
}
return json;
}

export function encodeJsonAsURI(json: unknown): string {
if (typeof json === 'string') {
return encodeJsonAsURI(JSON.parse(json));
}

const results: string[] = [];

function encodeAndStripWhitespace(key: string): string {
return encodeURIComponent(key.replace(' ', ''));
}

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
for (const [key, value] of Object.entries(json)) {
if (!value) {
continue;
}
const isBool = typeof value == 'boolean';
const isNumber = typeof value == 'number';
const isString = typeof value == 'string';
let encoded;
if (isBool || isNumber) {
encoded = `${encodeAndStripWhitespace(key)}=${value}`;
} else if (isString) {
encoded = `${encodeAndStripWhitespace(key)}=${encodeURIComponent(value)}`;
} else {
encoded = `${encodeAndStripWhitespace(key)}=${encodeURIComponent(JSON.stringify(value))}`;
}
results.push(encoded);
}
return results.join('&');
}
1 change: 1 addition & 0 deletions packages/common/lib/functions/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './CredentialOfferUtil';
export * from './Encoding';
3 changes: 1 addition & 2 deletions packages/common/lib/types/CredentialIssuance.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import {OpenId4VCIVersion} from "./OpenID4VCIVersions.types";
export interface CredentialRequest {
//TODO: handling list is out of scope for now
type: string | string[];
//TODO: handling list is out of scope for now
format: CredentialFormat | CredentialFormat[];
format: CredentialFormat;
proof: ProofOfPossession;
}

Expand Down
7 changes: 6 additions & 1 deletion packages/common/lib/types/Generic.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ export interface IssuerMetadata {
credential_endpoint: string;
batch_credential_endpoint?: string;
credentials_supported: CredentialIssuerMetadataSupportedCredentials[];
credential_issuer: Display;
credential_issuer: string;
authorization_server?: string
token_endpoint?: string
display?: (NameAndLocale & { [key: string]: string })[]
}

export interface CredentialIssuerMetadataSupportedCredentials {
Expand Down Expand Up @@ -99,6 +102,8 @@ export interface AuthorizationRequestJwtVcJsonLdAndLdpVc extends CommonAuthoriza
export interface CommonAuthorizationDetails {
type: 'openid_credential' | string;
format: CredentialFormatEnum;
// If the Credential Issuer metadata contains an authorization_server parameter, the authorization detail's locations common data field MUST be set to the Credential Issuer Identifier value.
locations?: string[];
}

export interface AuthorizationDetailsJwtVcJson extends CommonAuthorizationDetails {
Expand Down
2 changes: 0 additions & 2 deletions packages/common/lib/types/v1_0_09.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ export interface AuthorizationRequestV1_0_09 extends CommonAuthorizationRequest
}

export interface AuthorizationDetailsJwtVcJsonV1_0_09 extends CommonAuthorizationDetails {
// If the Credential Issuer metadata contains an authorization_server parameter, the authorization detail's locations common data field MUST be set to the Credential Issuer Identifier value.
locations?: string[];
types: string[];
// fixme: we don't support this property in the current flow for jff. so I commented it out
//CredentialSubject?: IssuerCredentialSubject;
Expand Down
3 changes: 2 additions & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"build": "tsc"
},
"dependencies": {
"@sphereon/ssi-types": "^0.9.0"
"@sphereon/ssi-types": "^0.9.0",
"jwt-decode": "^3.1.2"
},
"devDependencies": {
"@types/jest": "^29.5.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/common/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
"esModuleInterop": true,
"moduleResolution": "Node"
}
}
}
57 changes: 57 additions & 0 deletions packages/issuer/lib/VcIssuer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {CredentialRequest, CredentialResponse, getKidFromJWT, IssuerMetadata, TokenErrorResponse} from "@sphereon/openid4vci-common";
import { ICredential, W3CVerifiableCredential } from '@sphereon/ssi-types'

export class VcIssuer {
_issuerMetadata: IssuerMetadata
_userPinRequired?: boolean
constructor(issuerMetadata: IssuerMetadata, userPinRequired?: boolean) {
this._issuerMetadata = issuerMetadata
this._userPinRequired = userPinRequired
}

public getIssuerMetadata() {
return this._issuerMetadata
}

public async issueCredentialFromIssueRequest(issueCredentialRequest: CredentialRequest): Promise<CredentialResponse> {
//TODO: do we want additional validations here?
if (this.isMetadataSupportCredentialRequestFormat(issueCredentialRequest.format)) {
return await this.issueCredential(issueCredentialRequest)
}
throw new Error(TokenErrorResponse.invalid_request)
}

private isMetadataSupportCredentialRequestFormat(requestFormat: string | string[]): boolean {
for (const credentialSupported of this._issuerMetadata.credentials_supported) {
if (!Array.isArray(requestFormat) && credentialSupported.format === requestFormat) {
return true
}
else if (Array.isArray(requestFormat)) {
for (const format of requestFormat as string[]) {
if (credentialSupported.format === format) {
return true
}
}
}
}
return false
}

private async issueCredential(issueCredentialRequest: CredentialRequest): Promise<CredentialResponse> {
const credential: ICredential = {
'@context': ['https://www.w3.org/2018/credentials/v1'],
issuanceDate: new Date().toUTCString(),
issuer: process.env.issuer_did as string,
type: Array.isArray(issueCredentialRequest.type)?issueCredentialRequest.type: [issueCredentialRequest.type],
credentialSubject: {
id: getKidFromJWT(issueCredentialRequest.proof.jwt as string),
given_name: 'John Doe',
},
}
return {
//todo: sign the credential here
credential: credential as W3CVerifiableCredential,
format: issueCredentialRequest.format,
}
}
}
92 changes: 92 additions & 0 deletions packages/issuer/lib/__test__/VcIssuerBuilder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {
CredentialFormatEnum,
CredentialIssuerMetadataSupportedCredentials,
Display,
IssuerCredentialSubjectDisplay,
TokenErrorResponse
} from '@sphereon/openid4vci-common'

import { CredentialSupportedV1_11Builder, VcIssuerBuilder } from '../index'

describe('VcIssuer builder should', () => {
it('generate a VcIssuer', () => {
const credentialsSupported: CredentialIssuerMetadataSupportedCredentials = new CredentialSupportedV1_11Builder()
.withCryptographicSuitesSupported('ES256K')
.withCryptographicBindingMethod('did')
.withFormat(CredentialFormatEnum.jwt_vc_json)
.withId('UniversityDegree_JWT')
.withCredentialDisplay({
name: 'University Credential',
locale: 'en-US',
logo: {
url: 'https://exampleuniversity.com/public/logo.png',
alt_text: 'a square logo of a university',
},
background_color: '#12107c',
text_color: '#FFFFFF',
} as Display)
.withIssuerCredentialSubjectDisplay('given_name', {
name: 'given name',
locale: 'en-US',
} as IssuerCredentialSubjectDisplay)
.build()
const vcIssuer = new VcIssuerBuilder()
.withAuthorizationServer('https://authorization-server')
.withCredentialEndpoint('https://credential-endpoint')
.withCredentialIssuer('https://credential-issuer')
.withIssuerDisplay({
name: 'example issuer',
locale: 'en-US',
})
.withCredentialsSupported(credentialsSupported)
.build()

expect(vcIssuer.getIssuerMetadata().authorization_server).toEqual('https://authorization-server')
expect(vcIssuer.getIssuerMetadata().display).toBeDefined()
expect(vcIssuer.getIssuerMetadata().credentials_supported[0].id).toEqual('UniversityDegree_JWT')
})

it('fail to generate a VcIssuer', () => {
const credentialsSupported: CredentialIssuerMetadataSupportedCredentials = new CredentialSupportedV1_11Builder()
.withCryptographicSuitesSupported('ES256K')
.withCryptographicBindingMethod('did')
.withFormat(CredentialFormatEnum.jwt_vc_json)
.withId('UniversityDegree_JWT')
.withCredentialDisplay({
name: 'University Credential',
locale: 'en-US',
logo: {
url: 'https://exampleuniversity.com/public/logo.png',
alt_text: 'a square logo of a university',
},
background_color: '#12107c',
text_color: '#FFFFFF',
} as Display)
.withIssuerCredentialSubjectDisplay('given_name', {
name: 'given name',
locale: 'en-US',
} as IssuerCredentialSubjectDisplay)
.build()
expect(() =>
new VcIssuerBuilder()
.withAuthorizationServer('https://authorization-server')
.withCredentialEndpoint('https://credential-endpoint')
.withIssuerDisplay({
name: 'example issuer',
locale: 'en-US',
})
.withCredentialsSupported(credentialsSupported)
.build()
).toThrowError(TokenErrorResponse.invalid_request)
})

it('fail to generate a CredentialSupportedV1_11', () => {
expect(() =>
new CredentialSupportedV1_11Builder()
.withCryptographicSuitesSupported('ES256K')
.withCryptographicBindingMethod('did')
.withId('UniversityDegree_JWT')
.build()
).toThrowError(TokenErrorResponse.invalid_request)
})
})
Loading

0 comments on commit c2592a8

Please sign in to comment.