-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathDpopIDToken.js
More file actions
160 lines (132 loc) · 4.85 KB
/
Copy pathDpopIDToken.js
File metadata and controls
160 lines (132 loc) · 4.85 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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
/**
* Local dependencies
*/
const { JWT } = require('@solid/jose')
const { hashClaim, random } = require('./crypto')
const DEFAULT_MAX_AGE = 1209600 // Default ID token expiration, in seconds
const DEFAULT_SIG_ALGORITHM = 'RS256'
/**
* DpopIDToken
*/
class DpopIDToken extends JWT {
/**
* issue
*
* @param provider {Provider} OIDC Identity Provider issuing the token
* @param provider.issuer {string} Provider URI
* @param provider.keys {KeyChain}
*
* @param options {Object}
* @param options.aud {string|Array<string>} Audience for the token
* (such as the Relying Party client_id)
* @param options.azp {string} Authorized party / Presenter (RP client_id)
* @param options.sub {string} Subject id for the token (opaque, unique to
* the issuer)
* @param options.nonce {string} Nonce generated by Relying Party
*
* Optional:
* @param [options.alg] {string} Algorithm for signing the id token
* @param [options.jti] {string} Unique JWT id (to prevent reuse)
* @param [options.iat] {number} Issued at timestamp (in seconds)
* @param [options.max] {number} Max token lifetime in seconds
* @param [options.at_hash] {string} Access Token Hash
* @param [options.c_hash] {string} Code hash
* @param [options.cnf] {Object} Proof of Possession confirmation key, see
* https://tools.ietf.org/html/rfc7800#section-3.1
*
* @returns {DpopIDToken} ID Token (JWT instance)
*/
static issue (provider, options) {
let { issuer, keys } = provider
let { aud, azp, sub, at_hash, c_hash, cnf, scope } = options
// If audience is an array and azp wasn't provided, default azp to the
// first audience entry so DPoP ID tokens include azp when `aud` is an array.
if (Array.isArray(aud) && !azp) {
azp = aud[0]
}
let alg = options.alg || DEFAULT_SIG_ALGORITHM
let jti = options.jti || random(8)
let iat = options.iat || Math.floor(Date.now() / 1000)
let max = options.max || DEFAULT_MAX_AGE
let exp = iat + max // token expiration
let iss = issuer
let key = keys['id_token'].signing[alg].privateKey
let kid = keys['id_token'].signing[alg].publicJwk.kid
let header = { alg, kid }
let payload = { iss, aud, azp, sub, exp, iat, jti }
// Ensure azp is in payload when aud is an array (required by OIDC spec)
if (Array.isArray(aud) && !payload.azp) {
payload.azp = aud[0]
}
// Add webid claim for Solid OIDC compliance when webid scope is requested
if (sub && scope && (scope.includes('webid') || scope.split(' ').includes('webid'))) {
payload.webid = sub
}
if (at_hash) { payload.at_hash = at_hash }
if (c_hash) { payload.c_hash = c_hash }
if (cnf) { payload.cnf = cnf }
let jwt = new DpopIDToken({ header, payload, key })
return jwt
}
/**
* issueForRequest
*/
static issueForRequest (request, response) {
// TODO: Implement id_vc
let {params, code, provider, client, subject} = request
let alg = client['id_token_signed_response_alg'] || DEFAULT_SIG_ALGORITHM
let jti = random(8)
let iat = Math.floor(Date.now() / 1000)
let aud, azp, sub, max, scope
// authentication request
if (!code) {
aud = [client['client_id'], 'solid']
azp = client['client_id']
// Use WebID URL for sub if available (Solid OIDC compliance), otherwise use database ID
sub = subject?.webId || subject['_id']
max = parseInt(params['max_age']) || client['default_max_age'] || DEFAULT_MAX_AGE
scope = params.scope // Get the requested scope
// token request
} else {
// Ensure aud is array containing both client_id and 'solid'
if (Array.isArray(code.aud) && code.aud.includes('solid')) {
aud = code.aud
} else {
aud = [code.aud, 'solid']
}
azp = code.azp || (Array.isArray(aud) ? aud[0] : aud)
sub = code.sub
max = parseInt(code['max']) || client['default_max_age'] || DEFAULT_MAX_AGE
scope = code.scope // Get the scope from authorization code
}
let len = alg.match(/(256|384|512)$/)[0]
// generate hashes
return Promise.all([
hashClaim(response['access_token'], len),
hashClaim(response['code'], len)
])
// build the id_token
.then(hashes => {
let [at_hash, c_hash] = hashes
let options = { alg, aud, azp, sub, iat, jti, at_hash, c_hash, scope }
if (request.cnfKey) {
options.cnf = { jwk: request.cnfKey }
}
return DpopIDToken.issue(provider, options)
})
// sign id token
.then(jwt => jwt.encode())
// add to response
.then(compact => {
response['id_token'] = compact
})
// resolve the response
.then(() => response)
}
}
DpopIDToken.DEFAULT_MAX_AGE = DEFAULT_MAX_AGE
DpopIDToken.DEFAULT_SIG_ALGORITHM = DEFAULT_SIG_ALGORITHM
/**
* Export
*/
module.exports = DpopIDToken