@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,139 @@
|
|
|
1
|
+
import { ObjectUtils } from '@sphereon/ssi-types';
|
|
2
|
+
|
|
3
|
+
import { convertJsonToURI, post } from './functions';
|
|
4
|
+
import {
|
|
5
|
+
AccessTokenRequest,
|
|
6
|
+
AccessTokenRequestOpts,
|
|
7
|
+
AccessTokenResponse,
|
|
8
|
+
AuthorizationServerOpts,
|
|
9
|
+
ErrorResponse,
|
|
10
|
+
GrantTypes,
|
|
11
|
+
IssuanceInitiationRequestPayload,
|
|
12
|
+
IssuanceInitiationWithBaseUrl,
|
|
13
|
+
IssuerTokenEndpointOpts,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
export class AccessTokenClient {
|
|
17
|
+
// private _clientId?: string;
|
|
18
|
+
// private _authorizationServerUrl?: string;
|
|
19
|
+
|
|
20
|
+
public async acquireAccessTokenUsingIssuanceInitiation(
|
|
21
|
+
issuanceInitiation: IssuanceInitiationWithBaseUrl,
|
|
22
|
+
clientId: string,
|
|
23
|
+
opts?: AccessTokenRequestOpts
|
|
24
|
+
): Promise<AccessTokenResponse | ErrorResponse> {
|
|
25
|
+
const { issuanceInitiationRequest } = issuanceInitiation;
|
|
26
|
+
const reqOpts = {
|
|
27
|
+
isPinRequired: issuanceInitiationRequest.user_pin_required || false,
|
|
28
|
+
issuerOpts: { issuer: issuanceInitiationRequest.issuer },
|
|
29
|
+
asOpts: opts?.asOpts ? { ...opts.asOpts } : undefined,
|
|
30
|
+
};
|
|
31
|
+
return await this.acquireAccessTokenUsingRequest(await this.createAccessTokenRequest(issuanceInitiationRequest, clientId, opts), reqOpts);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public async acquireAccessTokenUsingRequest(
|
|
35
|
+
accessTokenRequest: AccessTokenRequest,
|
|
36
|
+
opts: { isPinRequired?: boolean; asOpts?: AuthorizationServerOpts; issuerOpts?: IssuerTokenEndpointOpts }
|
|
37
|
+
): Promise<AccessTokenResponse | ErrorResponse> {
|
|
38
|
+
this.validate(accessTokenRequest, opts?.isPinRequired);
|
|
39
|
+
const requestTokenURL = convertJsonToURI(accessTokenRequest, {
|
|
40
|
+
baseUrl: this.determineTokenURL(opts?.asOpts, opts?.issuerOpts),
|
|
41
|
+
});
|
|
42
|
+
return this.sendAuthCode(requestTokenURL, accessTokenRequest);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public async createAccessTokenRequest(
|
|
46
|
+
issuanceInitiationRequest: IssuanceInitiationRequestPayload,
|
|
47
|
+
clientId: string,
|
|
48
|
+
opts?: AccessTokenRequestOpts
|
|
49
|
+
): Promise<AccessTokenRequest> {
|
|
50
|
+
const request: Partial<AccessTokenRequest> = {
|
|
51
|
+
client_id: clientId,
|
|
52
|
+
};
|
|
53
|
+
if (issuanceInitiationRequest.user_pin_required) {
|
|
54
|
+
this.assertNumericPin(true, opts.pin);
|
|
55
|
+
request.user_pin = opts.pin;
|
|
56
|
+
}
|
|
57
|
+
if (issuanceInitiationRequest.pre_authorized_code) {
|
|
58
|
+
request.grant_type = GrantTypes.PRE_AUTHORIZED_CODE;
|
|
59
|
+
request.pre_authorized_code = issuanceInitiationRequest.pre_authorized_code;
|
|
60
|
+
}
|
|
61
|
+
if (issuanceInitiationRequest.op_state) {
|
|
62
|
+
if (issuanceInitiationRequest.pre_authorized_code) {
|
|
63
|
+
throw new Error('Cannot have both a pre_authorized_code and a op_state in the same initiation request');
|
|
64
|
+
}
|
|
65
|
+
request.grant_type = GrantTypes.AUTHORIZATION_CODE;
|
|
66
|
+
this.throwNotSupportedFlow();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return request as AccessTokenRequest;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private assertPreAuthorizedGrantType(grantType: GrantTypes): void {
|
|
73
|
+
if (GrantTypes.PRE_AUTHORIZED_CODE !== grantType) {
|
|
74
|
+
throw new Error("grant type must be 'urn:ietf:params:oauth:grant-type:pre-authorized_code'");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private assertNumericPin(isPinRequired?: boolean, pin?: number): void {
|
|
79
|
+
if (isPinRequired) {
|
|
80
|
+
if (!pin || pin < 0 || 99999999 < pin) {
|
|
81
|
+
throw new Error('A valid pin consisting of maximal 8 numeric characters must be present.');
|
|
82
|
+
}
|
|
83
|
+
} else if (pin) {
|
|
84
|
+
throw new Error('Cannot set a pin, when the pin is not required.');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private assertNonEmptyPreAuthorizedCode(accessTokenRequest: AccessTokenRequest): void {
|
|
89
|
+
if (!accessTokenRequest.pre_authorized_code) {
|
|
90
|
+
throw new Error('Pre-authorization must be proven by presenting the pre-authorized code. Code must be present.');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private assertNonEmptyClientId(accessTokenRequest: AccessTokenRequest): void {
|
|
95
|
+
if (!accessTokenRequest.client_id || accessTokenRequest.client_id.length < 1) {
|
|
96
|
+
throw new Error('The client Id must be present.');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private validate(accessTokenRequest: AccessTokenRequest, isPinRequired?: boolean): void {
|
|
101
|
+
if (accessTokenRequest.grant_type === GrantTypes.PRE_AUTHORIZED_CODE) {
|
|
102
|
+
this.assertPreAuthorizedGrantType(accessTokenRequest.grant_type);
|
|
103
|
+
this.assertNonEmptyPreAuthorizedCode(accessTokenRequest);
|
|
104
|
+
this.assertNumericPin(isPinRequired, accessTokenRequest.user_pin);
|
|
105
|
+
this.assertNonEmptyClientId(accessTokenRequest);
|
|
106
|
+
} else {
|
|
107
|
+
this.throwNotSupportedFlow();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async sendAuthCode(requestTokenURL: string, accessTokenRequest: AccessTokenRequest): Promise<AccessTokenResponse | ErrorResponse> {
|
|
112
|
+
const response = await post(requestTokenURL, accessTokenRequest);
|
|
113
|
+
return await response.json();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private determineTokenURL(asOpts?: AuthorizationServerOpts, issuerOpts?: IssuerTokenEndpointOpts): string {
|
|
117
|
+
if (!asOpts && !issuerOpts) {
|
|
118
|
+
throw new Error('Cannot determine token URL if no issuer and no Authorization Server values are present');
|
|
119
|
+
}
|
|
120
|
+
const url = asOpts
|
|
121
|
+
? this.creatTokenURLFromURL(asOpts.as, asOpts.tokenEndpoint)
|
|
122
|
+
: this.creatTokenURLFromURL(issuerOpts.issuer, issuerOpts.tokenEndpoint);
|
|
123
|
+
if (!url || !ObjectUtils.isString(url)) {
|
|
124
|
+
throw new Error('No authorization server token URL present. Cannot acquire access token');
|
|
125
|
+
}
|
|
126
|
+
return url;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private creatTokenURLFromURL(url: string, tokenEndpoint?: string): string {
|
|
130
|
+
const hostname = url.replace(/https?:\/\//, '').split('/')[0];
|
|
131
|
+
const endpoint = tokenEndpoint ? tokenEndpoint : '/token';
|
|
132
|
+
// We always require https
|
|
133
|
+
return `https://${hostname}${endpoint}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private throwNotSupportedFlow(): void {
|
|
137
|
+
throw new Error('Only pre-authorized-code flow is supported');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { CredentialFormat } from '@sphereon/ssi-types';
|
|
2
|
+
|
|
3
|
+
import CredentialRequestClientBuilder from './CredentialRequestClientBuilder';
|
|
4
|
+
import { createProofOfPossession, isValidURL, post } from './functions';
|
|
5
|
+
import { CredentialRequest, CredentialResponse, ErrorResponse, ProofOfPossession, ProofOfPossessionOpts, URL_NOT_VALID } from './types';
|
|
6
|
+
|
|
7
|
+
export class CredentialRequestClient {
|
|
8
|
+
_issuanceRequestOpts: Partial<{
|
|
9
|
+
credentialRequestUrl: string;
|
|
10
|
+
credentialType: string | string[];
|
|
11
|
+
format: CredentialFormat | CredentialFormat[];
|
|
12
|
+
proof: ProofOfPossession;
|
|
13
|
+
token: string;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
public getCredentialRequestUrl(): string {
|
|
17
|
+
return this._issuanceRequestOpts.credentialRequestUrl;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public constructor(builder: CredentialRequestClientBuilder) {
|
|
21
|
+
this._issuanceRequestOpts = { ...builder };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public static builder(): CredentialRequestClientBuilder {
|
|
25
|
+
return new CredentialRequestClientBuilder();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public async sendCredentialRequest(
|
|
29
|
+
request: CredentialRequest,
|
|
30
|
+
opts?: { overrideCredentialRequestUrl?: string; overrideToken?: string }
|
|
31
|
+
): Promise<CredentialResponse | ErrorResponse> {
|
|
32
|
+
const requestUrl: string = opts?.overrideCredentialRequestUrl
|
|
33
|
+
? opts.overrideCredentialRequestUrl
|
|
34
|
+
: this._issuanceRequestOpts.credentialRequestUrl;
|
|
35
|
+
if (!isValidURL(requestUrl)) {
|
|
36
|
+
throw new Error(URL_NOT_VALID);
|
|
37
|
+
}
|
|
38
|
+
const requestToken: string = opts?.overrideToken ? opts.overrideToken : this._issuanceRequestOpts.token;
|
|
39
|
+
const response = await post(requestUrl, request, requestToken);
|
|
40
|
+
const responseJson = await response.json();
|
|
41
|
+
if (responseJson.error) {
|
|
42
|
+
return { ...responseJson } as ErrorResponse;
|
|
43
|
+
}
|
|
44
|
+
return { ...responseJson } as CredentialResponse;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async createCredentialRequest(
|
|
48
|
+
proof: ProofOfPossession | ProofOfPossessionOpts,
|
|
49
|
+
opts?: {
|
|
50
|
+
credentialType?: string | string[];
|
|
51
|
+
format?: CredentialFormat | CredentialFormat[];
|
|
52
|
+
}
|
|
53
|
+
): Promise<CredentialRequest> {
|
|
54
|
+
const proofOfPossession = 'jwt' in proof ? proof : await createProofOfPossession(proof);
|
|
55
|
+
return {
|
|
56
|
+
type: opts?.credentialType ? opts.credentialType : this._issuanceRequestOpts.credentialType,
|
|
57
|
+
format: opts?.format ? opts.format : this._issuanceRequestOpts.format,
|
|
58
|
+
proof: proofOfPossession,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { CredentialFormat } from '@sphereon/ssi-types';
|
|
2
|
+
|
|
3
|
+
import { CredentialRequestClient } from './CredentialRequestClient';
|
|
4
|
+
import { convertURIToJsonObject } from './functions';
|
|
5
|
+
import { IssuanceInitiationRequestPayload } from './types';
|
|
6
|
+
|
|
7
|
+
export default class CredentialRequestClientBuilder {
|
|
8
|
+
credentialRequestUrl: string;
|
|
9
|
+
credentialType: string | string[];
|
|
10
|
+
format: CredentialFormat | CredentialFormat[];
|
|
11
|
+
|
|
12
|
+
static fromIssuanceInitiationURI(issuanceInitiation: string): CredentialRequestClientBuilder {
|
|
13
|
+
return CredentialRequestClientBuilder.fromIssuanceInitiationRequest(
|
|
14
|
+
convertURIToJsonObject(issuanceInitiation, {
|
|
15
|
+
arrayTypeProperties: ['credential_type'],
|
|
16
|
+
requiredProperties: ['issuer', 'credential_type'],
|
|
17
|
+
}) as IssuanceInitiationRequestPayload
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static fromIssuanceInitiationRequest(issuanceInitiation: IssuanceInitiationRequestPayload): CredentialRequestClientBuilder {
|
|
22
|
+
const builder = new CredentialRequestClientBuilder();
|
|
23
|
+
builder.withCredentialRequestUrl(issuanceInitiation.issuer);
|
|
24
|
+
builder.withCredentialType(issuanceInitiation.credential_type);
|
|
25
|
+
return builder;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
withCredentialRequestUrl(credentialRequestUrl: string): CredentialRequestClientBuilder {
|
|
29
|
+
this.credentialRequestUrl = credentialRequestUrl;
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
withCredentialType(credentialType: string | string[]): CredentialRequestClientBuilder {
|
|
34
|
+
this.credentialType = credentialType;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
withFormat(format: CredentialFormat | CredentialFormat[]): CredentialRequestClientBuilder {
|
|
39
|
+
this.format = format;
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
build(): CredentialRequestClient {
|
|
44
|
+
return new CredentialRequestClient(this);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { convertJsonToURI, convertURIToJsonObject } from './functions';
|
|
2
|
+
import { IssuanceInitiationRequestPayload, IssuanceInitiationWithBaseUrl } from './types';
|
|
3
|
+
|
|
4
|
+
export default class IssuanceInitiation {
|
|
5
|
+
static fromURI(issuanceInitiationURI: string): IssuanceInitiationWithBaseUrl {
|
|
6
|
+
if (!issuanceInitiationURI.includes('?')) {
|
|
7
|
+
throw new Error('Invalid Issuance Initiation Request Payload');
|
|
8
|
+
}
|
|
9
|
+
const baseUrl = issuanceInitiationURI.split('?')[0];
|
|
10
|
+
const issuanceInitiationRequest = convertURIToJsonObject(issuanceInitiationURI, {
|
|
11
|
+
arrayTypeProperties: ['credential_type'],
|
|
12
|
+
requiredProperties: ['issuer', 'credential_type'],
|
|
13
|
+
}) as IssuanceInitiationRequestPayload;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
baseUrl,
|
|
17
|
+
issuanceInitiationRequest,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static toURI(issuanceInitiation: IssuanceInitiationWithBaseUrl): string {
|
|
22
|
+
return convertJsonToURI(issuanceInitiation.issuanceInitiationRequest, {
|
|
23
|
+
baseUrl: issuanceInitiation.baseUrl,
|
|
24
|
+
arrayTypeProperties: ['credential_type'],
|
|
25
|
+
uriTypeProperties: ['issuer', 'credential_type'],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { BAD_PARAMS, DecodeURIAsJsonOpts, EncodeJsonAsURIOpts, SearchValue } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @function encodeJsonAsURI encodes a Json object into a URI
|
|
5
|
+
* @param json object
|
|
6
|
+
* @param opts:
|
|
7
|
+
* - urlTypeProperties: a list of properties of which the value is a URL
|
|
8
|
+
* - arrayTypeProperties: a list of properties which are an array
|
|
9
|
+
*/
|
|
10
|
+
export function convertJsonToURI(json: unknown, opts?: EncodeJsonAsURIOpts): string {
|
|
11
|
+
if (typeof json === 'string') {
|
|
12
|
+
return convertJsonToURI(JSON.parse(json), opts);
|
|
13
|
+
}
|
|
14
|
+
const results = [];
|
|
15
|
+
|
|
16
|
+
function encodeAndStripWhitespace(key: string): string {
|
|
17
|
+
return encodeURIComponent(key.replace(' ', ''));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const [key, value] of Object.entries(json)) {
|
|
21
|
+
if (!value) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
//Skip properties that are not of URL type
|
|
25
|
+
if (!opts?.uriTypeProperties?.includes(key)) {
|
|
26
|
+
results.push(`${key}=${value}`);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (opts?.arrayTypeProperties?.includes(key) && Array.isArray(value)) {
|
|
30
|
+
results.push(value.map((v) => `${encodeAndStripWhitespace(key)}=${customEncodeURIComponent(v, /\./g)}`).join('&'));
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const isBool = typeof value == 'boolean';
|
|
34
|
+
const isNumber = typeof value == 'number';
|
|
35
|
+
const isString = typeof value == 'string';
|
|
36
|
+
let encoded;
|
|
37
|
+
if (isBool || isNumber) {
|
|
38
|
+
encoded = `${encodeAndStripWhitespace(key)}=${value}`;
|
|
39
|
+
} else if (isString) {
|
|
40
|
+
encoded = `${encodeAndStripWhitespace(key)}=${customEncodeURIComponent(value, /\./g)}`;
|
|
41
|
+
} else {
|
|
42
|
+
encoded = `${encodeAndStripWhitespace(key)}=${customEncodeURIComponent(JSON.stringify(value), /\./g)}`;
|
|
43
|
+
}
|
|
44
|
+
results.push(encoded);
|
|
45
|
+
}
|
|
46
|
+
const components = results.join('&');
|
|
47
|
+
if (opts.baseUrl) {
|
|
48
|
+
return `${opts.baseUrl}?${components}`;
|
|
49
|
+
}
|
|
50
|
+
return components;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @function decodeUriAsJson decodes an URI into a Json object
|
|
55
|
+
* @param uri string
|
|
56
|
+
* @param opts:
|
|
57
|
+
* - requiredProperties: the required properties
|
|
58
|
+
* - arrayTypeProperties: properties that can show up more that once
|
|
59
|
+
*/
|
|
60
|
+
export function convertURIToJsonObject(uri: string, opts?: DecodeURIAsJsonOpts): unknown {
|
|
61
|
+
if (!uri || !opts?.requiredProperties.every((p) => uri.includes(p))) {
|
|
62
|
+
throw new Error(BAD_PARAMS);
|
|
63
|
+
}
|
|
64
|
+
const uriComponents = getURIComponentsAsArray(uri, opts?.arrayTypeProperties);
|
|
65
|
+
return decodeJsonProperties(uriComponents);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function decodeJsonProperties(parts: string[]): unknown {
|
|
69
|
+
const json: unknown = {};
|
|
70
|
+
for (const key in parts) {
|
|
71
|
+
const value = parts[key];
|
|
72
|
+
if (!value) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
if (value.length > 1) {
|
|
77
|
+
json[decodeURIComponent(key)] = value.map((v) => decodeURIComponent(v));
|
|
78
|
+
} else {
|
|
79
|
+
json[decodeURIComponent(key)] = decodeURIComponent(value[0]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const isBool = typeof value == 'boolean';
|
|
83
|
+
const isNumber = typeof value == 'number';
|
|
84
|
+
const isString = typeof value == 'string';
|
|
85
|
+
if (isBool || isNumber) {
|
|
86
|
+
json[decodeURIComponent(key)] = value;
|
|
87
|
+
} else if (isString) {
|
|
88
|
+
const decoded = decodeURIComponent(value);
|
|
89
|
+
if (decoded.startsWith('{') && decoded.endsWith('}')) {
|
|
90
|
+
json[decodeURIComponent(key)] = JSON.parse(decoded);
|
|
91
|
+
} else {
|
|
92
|
+
json[decodeURIComponent(key)] = decoded;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return json;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @function get URI Components as Array
|
|
101
|
+
* @param uri string
|
|
102
|
+
* @param arrayType array of string containing array like keys
|
|
103
|
+
*/
|
|
104
|
+
function getURIComponentsAsArray(uri: string, arrayType?: string[]): string[] {
|
|
105
|
+
const parts = uri.includes('?') ? uri.split('?')[1] : uri.includes('://') ? uri.split('://')[1] : uri;
|
|
106
|
+
const json: string[] = [];
|
|
107
|
+
const dict = parts.split('&');
|
|
108
|
+
for (const entry of dict) {
|
|
109
|
+
const pair = entry.split('=');
|
|
110
|
+
if (arrayType?.includes(pair[0])) {
|
|
111
|
+
if (json[pair[0]] !== undefined) {
|
|
112
|
+
json[pair[0]].push(pair[1]);
|
|
113
|
+
} else {
|
|
114
|
+
json[pair[0]] = [pair[1]];
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
json[pair[0]] = pair[1];
|
|
119
|
+
}
|
|
120
|
+
return json;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @function customEncodeURIComponent is used to encode chars that are not encoded by default
|
|
125
|
+
* @param searchValue The pattern/regexp to find the char(s) to be encoded
|
|
126
|
+
* @param uriComponent query string
|
|
127
|
+
*/
|
|
128
|
+
function customEncodeURIComponent(uriComponent: string, searchValue: SearchValue): string {
|
|
129
|
+
// -_.!~*'() are not escaped because they are considered safe.
|
|
130
|
+
// Add them to the regex as you need
|
|
131
|
+
return encodeURIComponent(uriComponent).replace(searchValue, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
132
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { fetch } from 'cross-fetch';
|
|
2
|
+
|
|
3
|
+
export async function post(url: string, body: unknown, bearerToken?: string): Promise<Response> {
|
|
4
|
+
let message = '';
|
|
5
|
+
try {
|
|
6
|
+
const payload: RequestInit = {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
body: JSON.stringify(body),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
if (bearerToken) {
|
|
12
|
+
payload.headers = {
|
|
13
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const response = await fetch(url, payload);
|
|
17
|
+
if (response && response.status && response.status < 400) {
|
|
18
|
+
return response;
|
|
19
|
+
} else {
|
|
20
|
+
if (response) {
|
|
21
|
+
message = `${response.status}:${response.statusText}, ${await response.text()}`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`${(error as Error).message}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw new Error('unexpected error: ' + message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isValidURL(url: string): boolean {
|
|
32
|
+
const urlPattern = new RegExp(
|
|
33
|
+
'^(https:\\/\\/)?' + // validate protocol
|
|
34
|
+
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name
|
|
35
|
+
'((\\d{1,3}\\.){3}\\d{1,3}))' + // validate OR ip (v4) address
|
|
36
|
+
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path
|
|
37
|
+
'(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string
|
|
38
|
+
'(\\#[-a-z\\d_]*)?$',
|
|
39
|
+
'i'
|
|
40
|
+
); // validate fragment locator
|
|
41
|
+
return !!urlPattern.test(url);
|
|
42
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { BAD_PARAMS, JWS_NOT_VALID, JWTHeader, JWTPayload, JWTSignerArgs, ProofOfPossession, ProofOfPossessionOpts, ProofType } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* createProofOfPossession creates and returns the ProofOfPossession object
|
|
5
|
+
* @param opts
|
|
6
|
+
* - jwtSignerArgs: The arguments to create the signature
|
|
7
|
+
* - jwtSignerCallback: function to sign the proof
|
|
8
|
+
* - jwtVerifyCallback: function to verify if JWT is valid
|
|
9
|
+
*/
|
|
10
|
+
export async function createProofOfPossession(opts: ProofOfPossessionOpts): Promise<ProofOfPossession> {
|
|
11
|
+
if (!opts.jwtSignerCallback || !opts.jwtSignerArgs) {
|
|
12
|
+
throw new Error(BAD_PARAMS);
|
|
13
|
+
}
|
|
14
|
+
const signerArgs = setJWSDefaults(opts.jwtSignerArgs, opts.credentialRequestUrl);
|
|
15
|
+
const jwt = await opts.jwtSignerCallback(signerArgs);
|
|
16
|
+
try {
|
|
17
|
+
if (opts.jwtVerifyCallback) {
|
|
18
|
+
const algorithm = opts.jwtSignerArgs.header.alg;
|
|
19
|
+
await opts.jwtVerifyCallback({ jws: jwt, key: opts.jwtSignerArgs.publicKey, algorithms: [algorithm] });
|
|
20
|
+
} else {
|
|
21
|
+
partiallyValidateJWS(jwt);
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
throw new Error(JWS_NOT_VALID);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
proof_type: ProofType.JWT,
|
|
28
|
+
jwt,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function partiallyValidateJWS(jws: string): void {
|
|
33
|
+
if (jws.split('.').length !== 3 || !jws.startsWith('ey')) {
|
|
34
|
+
throw new Error(JWS_NOT_VALID);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function setJWSDefaults(args: JWTSignerArgs, credentialRequestUrl?: string): JWTSignerArgs {
|
|
39
|
+
const now = +new Date();
|
|
40
|
+
const aud = args.payload.aud ? args.payload.aud : credentialRequestUrl;
|
|
41
|
+
if (!aud) {
|
|
42
|
+
throw new Error('No request url provider');
|
|
43
|
+
}
|
|
44
|
+
const defaultPayload: Partial<JWTPayload> = {
|
|
45
|
+
aud,
|
|
46
|
+
iat: args.payload.iat ? args.payload.iat : now / 1000,
|
|
47
|
+
exp: args.payload.exp ? args.payload.exp : (now + 5 * 60000) / 1000,
|
|
48
|
+
};
|
|
49
|
+
const defaultHeader: JWTHeader = {
|
|
50
|
+
alg: 'ES256',
|
|
51
|
+
typ: 'JWT',
|
|
52
|
+
};
|
|
53
|
+
args.payload = { ...defaultPayload, ...args.payload };
|
|
54
|
+
args.header = { ...defaultHeader, ...args.header };
|
|
55
|
+
return args;
|
|
56
|
+
}
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export enum GrantTypes {
|
|
2
|
+
AUTHORIZATION_CODE = 'authorization_code',
|
|
3
|
+
PRE_AUTHORIZED_CODE = 'urn:ietf:params:oauth:grant-type:pre-authorized_code',
|
|
4
|
+
PASSWORD = 'password',
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export enum Encoding {
|
|
8
|
+
FORM_URL_ENCODED = 'application/x-www-form-urlencoded',
|
|
9
|
+
UTF_8 = 'UTF-8',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export enum ResponseType {
|
|
13
|
+
AUTH_CODE = 'code',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AuthorizationServerOpts {
|
|
17
|
+
as?: string; // If not provided the issuer hostname will be used!
|
|
18
|
+
tokenEndpoint?: string; // Allows to override the default '/token' endpoint
|
|
19
|
+
clientId?: string; // If not provided a random clientId will be generated
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IssuerTokenEndpointOpts {
|
|
23
|
+
issuer: string;
|
|
24
|
+
tokenEndpoint?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AccessTokenRequestOpts {
|
|
28
|
+
asOpts?: AuthorizationServerOpts;
|
|
29
|
+
pin?: number;
|
|
30
|
+
// client_id?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AuthorizationRequest {
|
|
34
|
+
response_type: ResponseType.AUTH_CODE;
|
|
35
|
+
client_id: string;
|
|
36
|
+
redirect_uri: string;
|
|
37
|
+
scope?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AuthorizationGrantResponse {
|
|
41
|
+
grant_type: string;
|
|
42
|
+
code: string;
|
|
43
|
+
scope?: string;
|
|
44
|
+
state?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AccessTokenRequest {
|
|
48
|
+
client_id?: string;
|
|
49
|
+
code_verifier?: string;
|
|
50
|
+
grant_type: GrantTypes;
|
|
51
|
+
pre_authorized_code: string;
|
|
52
|
+
redirect_uri?: string;
|
|
53
|
+
scope?: string;
|
|
54
|
+
user_pin?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AccessTokenResponse {
|
|
58
|
+
access_token?: number; // integer
|
|
59
|
+
token_type?: string;
|
|
60
|
+
expires_in?: number; // in seconds
|
|
61
|
+
c_nonce?: string;
|
|
62
|
+
c_nonce_expires_in?: number; // in seconds
|
|
63
|
+
authorization_pending?: boolean;
|
|
64
|
+
interval?: number; // in seconds
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ErrorResponse extends Response {
|
|
68
|
+
error: string;
|
|
69
|
+
error_description?: string;
|
|
70
|
+
error_uri?: string;
|
|
71
|
+
state?: string;
|
|
72
|
+
}
|