@sphereon/oid4vci-client 0.0.1-unstable.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ import { KeyObject } from 'crypto';
2
+
3
+ import { CredentialFormat, W3CVerifiableCredential } from '@sphereon/ssi-types';
4
+
5
+ export enum AuthzFlowType {
6
+ AUTHORIZATION_CODE_FLOW = 'Authorization Code Flow',
7
+ PRE_AUTHORIZED_CODE_FLOW = 'Pre-Authorized Code Flow',
8
+ }
9
+
10
+ // eslint-disable-next-line @typescript-eslint/no-namespace
11
+ export namespace AuthzFlowType {
12
+ export function valueOf(request: IssuanceInitiationRequestPayload): AuthzFlowType {
13
+ if (request.pre_authorized_code) {
14
+ return AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW;
15
+ }
16
+ return AuthzFlowType.AUTHORIZATION_CODE_FLOW;
17
+ }
18
+ }
19
+
20
+ export interface CredentialRequest {
21
+ //TODO: handling list is out of scope for now
22
+ type: string | string[];
23
+ //TODO: handling list is out of scope for now
24
+ format: CredentialFormat | CredentialFormat[];
25
+ proof: ProofOfPossession;
26
+ }
27
+
28
+ export interface CredentialResponse {
29
+ credential: W3CVerifiableCredential;
30
+ format: CredentialFormat;
31
+ }
32
+
33
+ export interface IssuanceInitiationWithBaseUrl {
34
+ baseUrl: string;
35
+ issuanceInitiationRequest: IssuanceInitiationRequestPayload;
36
+ }
37
+
38
+ export interface IssuanceInitiationRequestPayload {
39
+ issuer: string; //(url) REQUIRED The issuer URL of the Credential issuer, the Wallet is requested to obtain one or more Credentials from.
40
+ credential_type: string[] | string; //(url) REQUIRED A JSON string denoting the type of the Credential the Wallet shall request
41
+ pre_authorized_code?: string; //CONDITIONAL the code representing the issuer's authorization for the Wallet to obtain Credentials of a certain type. This code MUST be short-lived and single-use. MUST be present in a pre-authorized code flow.
42
+ user_pin_required?: boolean; //OPTIONAL Boolean value specifying whether the issuer expects presentation of a user PIN along with the Token Request in a pre-authorized code flow. Default is false.
43
+ op_state?: string; //(JWT) OPTIONAL String value created by the Credential Issuer and opaque to the Wallet that is used to bind the subsequent authentication request with the Credential Issuer to a context set up during previous steps
44
+ }
45
+
46
+ export enum ProofType {
47
+ JWT = 'jwt',
48
+ }
49
+
50
+ export interface ProofOfPossession {
51
+ proof_type: ProofType;
52
+ jwt: string;
53
+
54
+ [x: string]: unknown;
55
+ }
56
+
57
+ export type SearchValue = {
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ [Symbol.replace](string: string, replacer: (substring: string, ...args: any[]) => string): string;
60
+ };
61
+
62
+ export type EncodeJsonAsURIOpts = { uriTypeProperties?: string[]; arrayTypeProperties?: string[]; baseUrl?: string };
63
+
64
+ export type DecodeURIAsJsonOpts = { requiredProperties?: string[]; arrayTypeProperties?: string[] };
65
+
66
+ export interface JWK {
67
+ kty?: string;
68
+ crv?: string;
69
+ x?: string;
70
+ y?: string;
71
+ e?: string;
72
+ n?: string;
73
+ }
74
+
75
+ export type Alg = 'ES256' | 'EdDSA';
76
+
77
+ export interface JWTHeader {
78
+ alg: Alg; // REQUIRED by the JWT signer
79
+ typ?: string; //JWT always
80
+ kid?: string; // CONDITIONAL. JWT header containing the key ID. If the Credential shall be bound to a DID, the kid refers to a DID URL which identifies a particular key in the DID Document that the Credential shall be bound to. MUST NOT be present if jwk or x5c is present.
81
+ jwk?: JWK; // CONDITIONAL. JWT header containing the key material the new Credential shall be bound to. MUST NOT be present if kid or x5c is present.
82
+ x5c?: string[]; // CONDITIONAL. JWT header containing a certificate or certificate chain corresponding to the key used to sign the JWT. This element may be used to convey a key attestation. In such a case, the actual key certificate will contain attributes related to the key properties. MUST NOT be present if kid or jwk is present.
83
+ }
84
+
85
+ export interface JWTPayload {
86
+ iss: string; // REQUIRED (string). The value of this claim MUST be the client_id of the client making the credential request.
87
+ aud?: string; // REQUIRED (string). The value of this claim MUST be the issuer URL of credential issuer.
88
+ iat?: number; // REQUIRED (number). The value of this claim MUST be the time at which the proof was issued using the syntax defined in [RFC7519].
89
+ nonce: string; // REQUIRED (string). The value type of this claim MUST be a string, where the value is a c_nonce provided by the credential issuer.
90
+ jti: string; // A new nonce chosen by the wallet. Used to prevent replay
91
+ exp?: number; // Not longer than 5 minutes
92
+ }
93
+
94
+ export interface JWTSignerArgs {
95
+ header: JWTHeader;
96
+ payload: JWTPayload;
97
+ privateKey: KeyObject;
98
+ publicKey: KeyObject;
99
+ }
100
+
101
+ export interface JWTVerifyArgs {
102
+ jws: string;
103
+ key: KeyObject;
104
+ algorithms?: Alg[];
105
+ }
106
+
107
+ export interface ProofOfPossessionOpts {
108
+ credentialRequestUrl: string;
109
+ jwtSignerArgs: JWTSignerArgs;
110
+ jwtSignerCallback: JWTSignerCallback;
111
+ jwtVerifyCallback?: JWTVerifyCallback;
112
+ }
113
+
114
+ export type JWTSignerCallback = (args: JWTSignerArgs) => Promise<string>;
115
+
116
+ export type JWTVerifyCallback = (args: JWTVerifyArgs) => Promise<void>;
117
+
118
+ export type Request = CredentialRequest;
@@ -0,0 +1,3 @@
1
+ export * from './OIDC4VCI.types';
2
+ export * from './Oidc4vciErrors';
3
+ export * from './VCIssuance.types';
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@sphereon/oid4vci-client",
3
+ "version": "0.0.1-unstable.1",
4
+ "description": "OpenID for Verifiable Credential Issuance (OID4VCI) client",
5
+ "main": "dist/main/index.js",
6
+ "author": "Sphereon",
7
+ "license": "Apache-2.0",
8
+ "private": false,
9
+ "scripts": {
10
+ "build": "run-p build:*",
11
+ "build:main": "tsc -p tsconfig.build.json",
12
+ "clean": "rimraf dist coverage",
13
+ "watch": "tsc -w",
14
+ "fix": "run-s fix:*",
15
+ "fix:prettier": "prettier \"{lib,tests}/**/*.ts\" --write",
16
+ "fix:lint": "eslint . --ext .ts --fix",
17
+ "test": "run-s build test:*",
18
+ "test:lint": "eslint . --ext .ts",
19
+ "test:prettier": "prettier \"{lib,tests}/**/*.ts\" --list-different",
20
+ "test:cov": "jest --ci --coverage && codecov",
21
+ "uninstall": "rimraf dist coverage yarn.lock package-lock.json node_modules"
22
+ },
23
+ "engines": {
24
+ "node": ">=14"
25
+ },
26
+ "dependencies": {
27
+ "@sphereon/ssi-types": "0.8.1-next.31",
28
+ "bs58": "^5.0.0",
29
+ "cross-fetch": "^3.1.5",
30
+ "uint8arrays": "^3.1.1"
31
+ },
32
+ "devDependencies": {
33
+ "@types/jest": "^28.1.8",
34
+ "@typescript-eslint/eslint-plugin": "^5.36.1",
35
+ "@typescript-eslint/parser": "^5.36.1",
36
+ "codecov": "^3.8.3",
37
+ "dotenv": "^16.0.2",
38
+ "eslint": "^8.23.0",
39
+ "eslint-config-prettier": "^8.5.0",
40
+ "eslint-plugin-eslint-comments": "^3.2.0",
41
+ "eslint-plugin-import": "^2.26.0",
42
+ "jest": "^29.1.2",
43
+ "jest-junit": "^14.0.1",
44
+ "jose": "^4.10.0",
45
+ "nock": "^13.2.9",
46
+ "npm-run-all": "^4.1.5",
47
+ "open-cli": "^7.0.1",
48
+ "rimraf": "^3.0.2",
49
+ "prettier": "^2.7.1",
50
+ "ts-jest": "^29.0.3",
51
+ "ts-node": "^10.9.1",
52
+ "typescript": "4.6.4"
53
+ },
54
+ "prettier": {
55
+ "singleQuote": true,
56
+ "printWidth": 150
57
+ },
58
+ "resolutions": {}
59
+ }
@@ -0,0 +1,149 @@
1
+ import nock from 'nock';
2
+
3
+ import { AccessTokenClient, AccessTokenRequest, AccessTokenResponse, GrantTypes } from '../lib';
4
+
5
+ import { UNIT_TEST_TIMEOUT } from './IT.spec';
6
+
7
+ const MOCK_URL = 'https://sphereonjunit20221013.com/';
8
+
9
+ describe('AccessTokenClient should', () => {
10
+ it(
11
+ 'get Access Token without resulting in errors',
12
+ async () => {
13
+ const accessTokenClient: AccessTokenClient = new AccessTokenClient();
14
+
15
+ const accessTokenIssuanceRequest: AccessTokenRequest = {
16
+ grant_type: GrantTypes.PRE_AUTHORIZED_CODE,
17
+ pre_authorized_code: '20221013',
18
+ client_id: 'sphereon',
19
+ } as AccessTokenRequest;
20
+
21
+ const body: AccessTokenResponse = {
22
+ access_token: 20221013,
23
+ authorization_pending: false,
24
+ c_nonce: 'c_nonce2022101300',
25
+ c_nonce_expires_in: 2022101300,
26
+ interval: 2022101300,
27
+ token_type: 'Bearer',
28
+ };
29
+ nock(MOCK_URL).post(/.*/).reply(200, JSON.stringify(body));
30
+
31
+ const accessTokenResponse: AccessTokenResponse = (await accessTokenClient.acquireAccessTokenUsingRequest(accessTokenIssuanceRequest, {
32
+ asOpts: { as: MOCK_URL },
33
+ })) as AccessTokenResponse;
34
+
35
+ expect(accessTokenResponse).toEqual(body);
36
+ },
37
+ UNIT_TEST_TIMEOUT
38
+ );
39
+
40
+ it(
41
+ 'get error',
42
+ async () => {
43
+ const accessTokenClient: AccessTokenClient = new AccessTokenClient();
44
+
45
+ const accessTokenIssuanceRequest: AccessTokenRequest = {
46
+ grant_type: GrantTypes.AUTHORIZATION_CODE,
47
+ } as AccessTokenRequest;
48
+
49
+ nock(MOCK_URL).post(/.*/).reply(200, '');
50
+
51
+ await expect(accessTokenClient.acquireAccessTokenUsingRequest(accessTokenIssuanceRequest, { asOpts: { as: MOCK_URL } })).rejects.toThrow(
52
+ 'Only pre-authorized-code flow is supported'
53
+ );
54
+ },
55
+ UNIT_TEST_TIMEOUT
56
+ );
57
+
58
+ it(
59
+ 'get error for incorrect code',
60
+ async () => {
61
+ const accessTokenClient: AccessTokenClient = new AccessTokenClient();
62
+
63
+ const accessTokenIssuanceRequest: AccessTokenRequest = {
64
+ grant_type: GrantTypes.PRE_AUTHORIZED_CODE,
65
+ pre_authorized_code: '',
66
+ user_pin: 1.0,
67
+ } as AccessTokenRequest;
68
+
69
+ nock(MOCK_URL).post(/.*/).reply(200, {});
70
+
71
+ await expect(accessTokenClient.acquireAccessTokenUsingRequest(accessTokenIssuanceRequest, { asOpts: { as: MOCK_URL } })).rejects.toThrow(
72
+ 'Pre-authorization must be proven by presenting the pre-authorized code. Code must be present.'
73
+ );
74
+ },
75
+ UNIT_TEST_TIMEOUT
76
+ );
77
+
78
+ it(
79
+ 'get error for incorrect pin',
80
+ async () => {
81
+ const accessTokenClient: AccessTokenClient = new AccessTokenClient();
82
+
83
+ const accessTokenIssuanceRequest: AccessTokenRequest = {
84
+ grant_type: GrantTypes.PRE_AUTHORIZED_CODE,
85
+ pre_authorized_code: '20221013',
86
+ user_pin: null,
87
+ } as AccessTokenRequest;
88
+
89
+ nock(MOCK_URL).post(/.*/).reply(200, {});
90
+
91
+ await expect(
92
+ accessTokenClient.acquireAccessTokenUsingRequest(accessTokenIssuanceRequest, { isPinRequired: true, asOpts: { as: MOCK_URL } })
93
+ ).rejects.toThrow('A valid pin consisting of maximal 8 numeric characters must be present.');
94
+ },
95
+ UNIT_TEST_TIMEOUT
96
+ );
97
+
98
+ it(
99
+ 'get error for incorrect client id',
100
+ async () => {
101
+ const accessTokenClient: AccessTokenClient = new AccessTokenClient();
102
+
103
+ const accessTokenIssuanceRequest: AccessTokenRequest = {
104
+ grant_type: GrantTypes.PRE_AUTHORIZED_CODE,
105
+ pre_authorized_code: '20221013',
106
+ user_pin: 20221013,
107
+ } as AccessTokenRequest;
108
+
109
+ nock(MOCK_URL).post(/.*/).reply(200, {});
110
+
111
+ await expect(
112
+ accessTokenClient.acquireAccessTokenUsingRequest(accessTokenIssuanceRequest, { isPinRequired: true, asOpts: { as: MOCK_URL } })
113
+ ).rejects.toThrow('The client Id must be present.');
114
+ },
115
+ UNIT_TEST_TIMEOUT
116
+ );
117
+ it(
118
+ 'get error for incorrectly long pin',
119
+ async () => {
120
+ const accessTokenClient: AccessTokenClient = new AccessTokenClient();
121
+
122
+ const accessTokenIssuanceRequest: AccessTokenRequest = {
123
+ grant_type: GrantTypes.PRE_AUTHORIZED_CODE,
124
+ pre_authorized_code: '20221013',
125
+ client_id: 'spheroen.com',
126
+ user_pin: 123456789,
127
+ } as AccessTokenRequest;
128
+
129
+ nock(MOCK_URL).post(/.*/).reply(200, {});
130
+
131
+ await expect(
132
+ accessTokenClient.acquireAccessTokenUsingRequest(accessTokenIssuanceRequest, { isPinRequired: true, asOpts: { as: MOCK_URL } })
133
+ ).rejects.toThrow(Error('A valid pin consisting of maximal 8 numeric characters must be present.'));
134
+ },
135
+ UNIT_TEST_TIMEOUT
136
+ );
137
+
138
+ it(
139
+ 'get error for unsupported flow type',
140
+ async () => {
141
+ const accessTokenClient: AccessTokenClient = new AccessTokenClient();
142
+
143
+ await expect(accessTokenClient.acquireAccessTokenUsingRequest({} as AccessTokenRequest, {})).rejects.toThrow(
144
+ Error('Only pre-authorized-code flow is supported')
145
+ );
146
+ },
147
+ UNIT_TEST_TIMEOUT
148
+ );
149
+ });
@@ -0,0 +1,11 @@
1
+ export const UNIT_TEST_TIMEOUT = 30000;
2
+
3
+ describe('oidc4vci client should', () => {
4
+ it(
5
+ 'succeed in starting',
6
+ async () => {
7
+ expect(true).toBeTruthy();
8
+ },
9
+ UNIT_TEST_TIMEOUT
10
+ );
11
+ });
@@ -0,0 +1,141 @@
1
+ import { AuthzFlowType, convertJsonToURI, convertURIToJsonObject } from '../lib';
2
+ import IssuanceInitiation from '../lib/IssuanceInitiation';
3
+
4
+ describe('JSON To URI', () => {
5
+ it('should parse an object into open-id-URI with a single credential_type', () => {
6
+ expect(
7
+ convertJsonToURI(
8
+ {
9
+ issuer: 'https://server.example.com',
10
+ credential_type: 'https://did.example.org/healthCard',
11
+ op_state: 'eyJhbGciOiJSU0Et...FYUaBy',
12
+ },
13
+ {
14
+ uriTypeProperties: ['issuer', 'credential_type'],
15
+ }
16
+ )
17
+ ).toEqual(
18
+ 'issuer=https%3A%2F%2Fserver%2Eexample%2Ecom&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FhealthCard&op_state=eyJhbGciOiJSU0Et...FYUaBy'
19
+ );
20
+ });
21
+ it('should parse an object into open-id-URI with an array of credential_type', () => {
22
+ expect(
23
+ convertJsonToURI(
24
+ {
25
+ issuer: 'https://server.example.com',
26
+ credential_type: ['https://did.example.org/healthCard', 'https://did.example.org/driverLicense'],
27
+ op_state: 'eyJhbGciOiJSU0Et...FYUaBy',
28
+ },
29
+ {
30
+ arrayTypeProperties: ['credential_type'],
31
+ uriTypeProperties: ['issuer', 'credential_type'],
32
+ }
33
+ )
34
+ ).toEqual(
35
+ 'issuer=https%3A%2F%2Fserver%2Eexample%2Ecom&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FhealthCard&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FdriverLicense&op_state=eyJhbGciOiJSU0Et...FYUaBy'
36
+ );
37
+ });
38
+ it('should parse an object into open-id-URI with an array of credential_type and json string', () => {
39
+ expect(
40
+ convertJsonToURI(
41
+ JSON.stringify({
42
+ issuer: 'https://server.example.com',
43
+ credential_type: ['https://did.example.org/healthCard', 'https://did.example.org/driverLicense'],
44
+ op_state: 'eyJhbGciOiJSU0Et...FYUaBy',
45
+ }),
46
+ {
47
+ arrayTypeProperties: ['credential_type'],
48
+ uriTypeProperties: ['issuer', 'credential_type'],
49
+ }
50
+ )
51
+ ).toEqual(
52
+ 'issuer=https%3A%2F%2Fserver%2Eexample%2Ecom&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FhealthCard&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FdriverLicense&op_state=eyJhbGciOiJSU0Et...FYUaBy'
53
+ );
54
+ });
55
+ });
56
+ describe('URI To Json Object', () => {
57
+ it('should parse open-id-URI as json object with a single credential_type', () => {
58
+ expect(
59
+ convertURIToJsonObject(
60
+ 'issuer=https%3A%2F%2Fserver%2Eexample%2Ecom&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FhealthCard&op_state=eyJhbGciOiJSU0Et...FYUaBy',
61
+ {
62
+ arrayTypeProperties: ['credential_type'],
63
+ requiredProperties: ['issuer', 'credential_type'],
64
+ }
65
+ )
66
+ ).toEqual({
67
+ issuer: 'https://server.example.com',
68
+ credential_type: 'https://did.example.org/healthCard',
69
+ op_state: 'eyJhbGciOiJSU0Et...FYUaBy',
70
+ });
71
+ });
72
+ it('should parse open-id-URI as json object with an array of credential_type', () => {
73
+ expect(
74
+ convertURIToJsonObject(
75
+ 'issuer=https%3A%2F%2Fserver%2Eexample%2Ecom&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FhealthCard&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FdriverLicense&op_state=eyJhbGciOiJSU0Et...FYUaBy',
76
+ {
77
+ arrayTypeProperties: ['credential_type'],
78
+ requiredProperties: ['issuer', 'credential_type'],
79
+ }
80
+ )
81
+ ).toEqual({
82
+ issuer: 'https://server.example.com',
83
+ credential_type: ['https://did.example.org/healthCard', 'https://did.example.org/driverLicense'],
84
+ op_state: 'eyJhbGciOiJSU0Et...FYUaBy',
85
+ });
86
+ });
87
+ });
88
+
89
+ describe('Authorization Request', () => {
90
+ it('Should return Issuance Initiation Request with base URL from URI', () => {
91
+ expect(
92
+ IssuanceInitiation.fromURI(
93
+ 'https://server.example.com?issuer=https%3A%2F%2Fserver%2Eexample%2Ecom&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FhealthCard&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FdriverLicense&op_state=eyJhbGciOiJSU0Et...FYUaBy'
94
+ )
95
+ ).toEqual({
96
+ baseUrl: 'https://server.example.com',
97
+ issuanceInitiationRequest: {
98
+ credential_type: ['https://did.example.org/healthCard', 'https://did.example.org/driverLicense'],
99
+ issuer: 'https://server.example.com',
100
+ op_state: 'eyJhbGciOiJSU0Et...FYUaBy',
101
+ },
102
+ });
103
+ });
104
+ });
105
+
106
+ describe('Authorization Flow Type determination', () => {
107
+ it('should return authorization code flow type with a single credential_type', () => {
108
+ expect(
109
+ AuthzFlowType.valueOf({
110
+ issuer: 'test',
111
+ credential_type: 'test',
112
+ })
113
+ ).toEqual(AuthzFlowType.AUTHORIZATION_CODE_FLOW);
114
+ });
115
+ it('should return authorization code flow type with a credential_type array', () => {
116
+ expect(
117
+ AuthzFlowType.valueOf({
118
+ issuer: 'test',
119
+ credential_type: ['test', 'test1'],
120
+ })
121
+ ).toEqual(AuthzFlowType.AUTHORIZATION_CODE_FLOW);
122
+ });
123
+ it('should return pre-authorized code flow with a single credential_type', () => {
124
+ expect(
125
+ AuthzFlowType.valueOf({
126
+ issuer: 'test',
127
+ credential_type: 'test',
128
+ pre_authorized_code: 'test',
129
+ })
130
+ ).toEqual(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW);
131
+ });
132
+ it('should return pre-authorized code flow with a credential_type array', () => {
133
+ expect(
134
+ AuthzFlowType.valueOf({
135
+ issuer: 'test',
136
+ credential_type: ['test', 'test1'],
137
+ pre_authorized_code: 'test',
138
+ })
139
+ ).toEqual(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW);
140
+ });
141
+ });
@@ -0,0 +1,159 @@
1
+ import { KeyObject } from 'crypto';
2
+
3
+ import * as jose from 'jose';
4
+ import { KeyLike, VerifyOptions } from 'jose/dist/types/types';
5
+ import nock from 'nock';
6
+ import * as u8a from 'uint8arrays';
7
+
8
+ import {
9
+ createProofOfPossession,
10
+ CredentialRequest,
11
+ CredentialRequestClient,
12
+ CredentialResponse,
13
+ ErrorResponse,
14
+ JWS_NOT_VALID,
15
+ JWTSignerArgs,
16
+ ProofOfPossession,
17
+ } from '../lib';
18
+
19
+ const partialJWT =
20
+ 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMS9rZXlzLzEifQ.eyJhdWQiOiJodHRwczovL29pZGM0dmNpLmRlbW8uc3BydWNlaWQuY29tL2NyZWRlbnRpYWwiLCJpYXQiOjE2';
21
+
22
+ // Must be JWS
23
+ const signJWT = async (args: JWTSignerArgs): Promise<string> => {
24
+ const { header, payload, privateKey } = args;
25
+ return await new jose.CompactSign(u8a.fromString(JSON.stringify({ ...payload })))
26
+ .setProtectedHeader({ ...header, alg: args.header.alg })
27
+ .sign(privateKey);
28
+ };
29
+
30
+ const verifyJWT = async (args: { jws: string | Uint8Array; key: KeyLike | Uint8Array; options?: VerifyOptions }): Promise<void> => {
31
+ await jose.compactVerify(args.jws, args.key, args.options);
32
+ };
33
+
34
+ const jwtArgs: JWTSignerArgs = {
35
+ header: {
36
+ alg: 'ES256',
37
+ kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1',
38
+ },
39
+ payload: {
40
+ iss: 's6BhdRkqt3',
41
+ nonce: 'tZignsnFbp',
42
+ jti: 'tZignsnFbp223',
43
+ },
44
+ privateKey: undefined,
45
+ publicKey: undefined,
46
+ };
47
+
48
+ beforeAll(async () => {
49
+ const keyPair = await jose.generateKeyPair('ES256');
50
+ jwtArgs.privateKey = keyPair.privateKey as KeyObject;
51
+ jwtArgs.publicKey = keyPair.publicKey as KeyObject;
52
+ });
53
+
54
+ describe('VcIssuanceClient ', () => {
55
+ it('should build correctly provided with correct params', function () {
56
+ const vcIssuanceClient = CredentialRequestClient.builder()
57
+ .withCredentialRequestUrl('https://oidc4vci.demo.spruceid.com/credential')
58
+ .withFormat('jwt_vc')
59
+ .build();
60
+ expect(vcIssuanceClient._issuanceRequestOpts.credentialRequestUrl).toBe('https://oidc4vci.demo.spruceid.com/credential');
61
+ });
62
+
63
+ it('should build credential request correctly', async () => {
64
+ const vcIssuanceClient = CredentialRequestClient.builder()
65
+ .withCredentialRequestUrl('https://oidc4vci.demo.spruceid.com/credential')
66
+ .withFormat('jwt_vc')
67
+ .withCredentialType('https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential')
68
+ .build();
69
+ const proof: ProofOfPossession = await createProofOfPossession({
70
+ credentialRequestUrl: vcIssuanceClient.getCredentialRequestUrl(),
71
+ jwtSignerArgs: jwtArgs,
72
+ jwtSignerCallback: (args) => signJWT(args),
73
+ jwtVerifyCallback: (args) => verifyJWT(args),
74
+ });
75
+ const credentialRequest: CredentialRequest = await vcIssuanceClient.createCredentialRequest(proof);
76
+ expect(credentialRequest.proof.jwt).toContain(partialJWT);
77
+ expect(credentialRequest.type).toBe('https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential');
78
+ });
79
+
80
+ it('should get fail credential response', async function () {
81
+ const basePath = 'https://sphereonjunit2022101301.com/';
82
+
83
+ nock(basePath).post(/.*/).reply(200, {
84
+ error: 'unsupported_format',
85
+ error_description: 'This is a mock error message',
86
+ });
87
+
88
+ const vcIssuanceClient = CredentialRequestClient.builder()
89
+ .withCredentialRequestUrl(basePath + '/credential')
90
+ .withFormat('ldp_vc')
91
+ .withCredentialType('https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential')
92
+ .build();
93
+ const proof: ProofOfPossession = await createProofOfPossession({
94
+ credentialRequestUrl: vcIssuanceClient.getCredentialRequestUrl(),
95
+ jwtSignerArgs: jwtArgs,
96
+ jwtSignerCallback: (args) => signJWT(args),
97
+ jwtVerifyCallback: (args) => verifyJWT(args),
98
+ });
99
+ const credentialRequest: CredentialRequest = await vcIssuanceClient.createCredentialRequest(proof);
100
+ expect(credentialRequest.proof.jwt.includes(partialJWT)).toBeTruthy();
101
+ const result: ErrorResponse | CredentialResponse = await vcIssuanceClient.sendCredentialRequest(credentialRequest);
102
+ expect(result['error']).toBe('unsupported_format');
103
+ });
104
+
105
+ it('should get success credential response', async function () {
106
+ nock('https://oidc4vci.demo.spruceid.com')
107
+ .post(/credential/)
108
+ .reply(200, {
109
+ format: 'jwt-vc',
110
+ credential:
111
+ 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzU2NTA0OSIsImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.z5vgMTK1nfizNCg5N-niCOL3WUIAL7nXy-nGhDZYO_-PNGeE-0djCpWAMH8fD8eWSID5PfkPBYkx_dfLJnQ7NA',
112
+ });
113
+ const vcIssuanceClient = CredentialRequestClient.builder()
114
+ .withCredentialRequestUrl('https://oidc4vci.demo.spruceid.com/credential')
115
+ .withFormat('jwt_vc')
116
+ .withCredentialType('https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential')
117
+ .build();
118
+ const proof: ProofOfPossession = await createProofOfPossession({
119
+ credentialRequestUrl: vcIssuanceClient.getCredentialRequestUrl(),
120
+ jwtSignerArgs: jwtArgs,
121
+ jwtSignerCallback: (args) => signJWT(args),
122
+ });
123
+ const credentialRequest: CredentialRequest = await vcIssuanceClient.createCredentialRequest(proof);
124
+ expect(credentialRequest.proof.jwt.includes(partialJWT)).toBeTruthy();
125
+ const result: ErrorResponse | CredentialResponse = await vcIssuanceClient.sendCredentialRequest(credentialRequest);
126
+ expect(result['credential']).toBeDefined();
127
+ });
128
+ it('should fail creating a proof of possession with simple verification', async () => {
129
+ const vcIssuanceClient = CredentialRequestClient.builder()
130
+ .withCredentialRequestUrl('https://oidc4vci.demo.spruceid.com/credential')
131
+ .withFormat('jwt_vc')
132
+ .withCredentialType('https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential')
133
+ .build();
134
+ await expect(
135
+ createProofOfPossession({
136
+ credentialRequestUrl: vcIssuanceClient.getCredentialRequestUrl(),
137
+ jwtSignerArgs: jwtArgs,
138
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
139
+ jwtSignerCallback: (_args) => Promise.resolve('invalid_jws'),
140
+ })
141
+ ).rejects.toThrow(Error(JWS_NOT_VALID));
142
+ });
143
+ it('should fail creating a proof of possession with verify callback function', async () => {
144
+ const vcIssuanceClient = CredentialRequestClient.builder()
145
+ .withCredentialRequestUrl('https://oidc4vci.demo.spruceid.com/credential')
146
+ .withFormat('jwt_vc')
147
+ .withCredentialType('https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential')
148
+ .build();
149
+ await expect(
150
+ createProofOfPossession({
151
+ credentialRequestUrl: vcIssuanceClient.getCredentialRequestUrl(),
152
+ jwtSignerArgs: jwtArgs,
153
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
154
+ jwtSignerCallback: (_args) => Promise.resolve('invalid_jws'),
155
+ jwtVerifyCallback: (args) => verifyJWT(args),
156
+ })
157
+ ).rejects.toThrow(Error(JWS_NOT_VALID));
158
+ });
159
+ });
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "./tsconfig",
3
+ "exclude": [
4
+ "tests/**/*.ts"
5
+ ]
6
+ }