-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathjwt.js
More file actions
124 lines (106 loc) · 3.23 KB
/
jwt.js
File metadata and controls
124 lines (106 loc) · 3.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/*!
* Copyright 2023 - 2024 California Department of Motor Vehicles
* Copyright 2023 - 2024 Digital Bazaar, Inc.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
import * as JWT from '@digitalbazaar/minimal-jwt';
import {config} from '@bedrock/core';
import crypto from 'node:crypto';
import jp from 'jsonpath';
/**
* Generates a JWT id_token from a VP exchange if the exchange is complete.
* @param {import("mongodb").Document}
* @param {import("../configs/config.js").RelyingParty} rp
*/
export const jwtFromExchange = async (exchange, rp) => {
const signingKey = config.opencred.signingKeys?.find(sk =>
sk.purpose.includes('id_token'),
);
if(!signingKey) {
throw new Error('No signing key found in config with purpose id_token');
}
const {privateKeyPem} = signingKey;
const rehydratedKey = crypto.createPrivateKey(privateKeyPem);
const signFn = async ({data}) => {
let algorithm;
if(signingKey.type === 'RS256') {
algorithm = 'RSA-SHA256';
} else if(signingKey.type === 'ES256') {
algorithm = 'SHA256';
} else {
throw new Error('Unsupported algorithm');
}
const sign = crypto.createSign(algorithm);
sign.write(data);
sign.end();
const sig = sign.sign(rehydratedKey, 'base64url');
return sig;
};
const header = {
alg: signingKey.type,
typ: 'JWT',
kid: signingKey.id,
};
if(!exchange.variables?.results) {
return null;
}
const stepResultKey = Object.keys(exchange.variables.results).find(
v => v == exchange.step,
);
const stepResults = exchange.variables.results[stepResultKey];
// Handle DC API workflow differently
if(rp.workflow?.type === 'dc-api' && !!stepResults) {
try {
const now = Math.floor(Date.now() / 1000);
// Default to 1 hour
const expirySeconds = rp.idTokenExpirySeconds || 3600;
const subject = stepResults.response['org.iso.18013.5.1'].document_number;
const verified =
stepResults.response.issuer_authentication == 'Valid' &&
stepResults.response.device_authentication == 'Valid';
const errors = stepResults.response.errors;
const payload = {
iss: config.server.baseUri,
aud: rp.clientId,
sub: subject || exchange.id,
iat: now,
exp: now + expirySeconds,
verified,
verification_method: 'dc-api',
verified_credentials: stepResults.response,
};
if(errors !== null) {
payload.errors = errors;
}
const jwt = await JWT.sign({payload, header, signFn});
return jwt.toString();
} catch(error) {
console.error('Error in DC API JWT generation:', error);
throw error;
}
}
const c = jp.query(
stepResults,
'$.verifiablePresentation.verifiableCredential[0]',
);
if(!c.length) {
return null;
}
const now = Math.floor(Date.now() / 1000);
const payload = {
iss: config.server.baseUri,
aud: rp.clientId,
sub: c[0].credentialSubject.id,
iat: now,
exp: now + rp.idTokenExpirySeconds,
};
for(const {name, path} of rp.claims ?? []) {
const claim = jp.query(c[0].credentialSubject, path);
if(claim) {
payload[name] = claim[0];
}
}
const jwt = await JWT.sign({payload, header, signFn});
return jwt.toString();
};