@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.
- package/.eslintrc.json +77 -0
- package/.github/workflows/main.yml +34 -0
- package/.prettierignore +3 -0
- package/CHANGELOG.md +5 -0
- package/LICENSE +201 -0
- package/README.md +206 -0
- package/dist/main/index.js +1 -0
- package/docs/preauthorized-code-flow.puml +38 -0
- package/index.ts +0 -0
- package/jest.config.cjs +26 -0
- package/lib/AccessTokenClient.ts +139 -0
- package/lib/CredentialRequestClient.ts +61 -0
- package/lib/CredentialRequestClientBuilder.ts +46 -0
- package/lib/IssuanceInitiation.ts +28 -0
- package/lib/functions/Encoding.ts +132 -0
- package/lib/functions/HttpUtils.ts +42 -0
- package/lib/functions/ProofUtil.ts +56 -0
- package/lib/functions/index.ts +3 -0
- package/lib/index.ts +6 -0
- package/lib/types/OIDC4VCI.types.ts +72 -0
- package/lib/types/Oidc4vciErrors.ts +3 -0
- package/lib/types/VCIssuance.types.ts +118 -0
- package/lib/types/index.ts +3 -0
- package/package.json +59 -0
- package/tests/AccessTokenClient.spec.ts +149 -0
- package/tests/IT.spec.ts +11 -0
- package/tests/IssuanceInitiation.spec.ts +141 -0
- package/tests/VcIssuanceClient.spec.ts +159 -0
- package/tsconfig.build.json +6 -0
- package/tsconfig.json +52 -0
|
@@ -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;
|
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
|
+
});
|
package/tests/IT.spec.ts
ADDED
|
@@ -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
|
+
});
|