@atproto/oauth-provider 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +29 -0
- package/dist/account/account.d.ts +6 -2
- package/dist/account/account.d.ts.map +1 -1
- package/dist/assets/app/bundle-manifest.json +3 -3
- package/dist/assets/app/main.css +1 -1
- package/dist/assets/app/main.js +1 -1
- package/dist/assets/app/main.js.map +1 -1
- package/dist/assets/assets-middleware.d.ts +2 -1
- package/dist/assets/assets-middleware.d.ts.map +1 -1
- package/dist/assets/assets-middleware.js +7 -0
- package/dist/assets/assets-middleware.js.map +1 -1
- package/dist/client/client-manager.d.ts +4 -3
- package/dist/client/client-manager.d.ts.map +1 -1
- package/dist/client/client-manager.js +60 -37
- package/dist/client/client-manager.js.map +1 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +1 -3
- package/dist/client/client.js.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -1
- package/dist/constants.js.map +1 -1
- package/dist/device/device-manager.d.ts +1 -1
- package/dist/device/device-manager.d.ts.map +1 -1
- package/dist/device/device-manager.js +2 -2
- package/dist/device/device-manager.js.map +1 -1
- package/dist/errors/invalid-authorization-details-error.d.ts +4 -3
- package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
- package/dist/errors/invalid-authorization-details-error.js +4 -4
- package/dist/errors/invalid-authorization-details-error.js.map +1 -1
- package/dist/lib/http/request.d.ts +3 -0
- package/dist/lib/http/request.d.ts.map +1 -1
- package/dist/lib/http/request.js +24 -12
- package/dist/lib/http/request.js.map +1 -1
- package/dist/metadata/build-metadata.d.ts +0 -1
- package/dist/metadata/build-metadata.d.ts.map +1 -1
- package/dist/metadata/build-metadata.js +9 -35
- package/dist/metadata/build-metadata.js.map +1 -1
- package/dist/oauth-hooks.d.ts +3 -10
- package/dist/oauth-hooks.d.ts.map +1 -1
- package/dist/oauth-provider.d.ts +8 -13
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +169 -109
- package/dist/oauth-provider.js.map +1 -1
- package/dist/oauth-verifier.d.ts +1 -2
- package/dist/oauth-verifier.d.ts.map +1 -1
- package/dist/oauth-verifier.js.map +1 -1
- package/dist/output/build-authorize-data.d.ts +6 -0
- package/dist/output/build-authorize-data.d.ts.map +1 -1
- package/dist/output/build-authorize-data.js +1 -0
- package/dist/output/build-authorize-data.js.map +1 -1
- package/dist/replay/replay-manager.d.ts +1 -0
- package/dist/replay/replay-manager.d.ts.map +1 -1
- package/dist/replay/replay-manager.js +3 -0
- package/dist/replay/replay-manager.js.map +1 -1
- package/dist/replay/replay-store.d.ts +1 -1
- package/dist/request/request-info.d.ts +2 -0
- package/dist/request/request-info.d.ts.map +1 -1
- package/dist/request/request-manager.d.ts +3 -9
- package/dist/request/request-manager.d.ts.map +1 -1
- package/dist/request/request-manager.js +52 -77
- package/dist/request/request-manager.js.map +1 -1
- package/dist/request/types.d.ts +10 -10
- package/dist/signer/signed-token-payload.d.ts +85 -85
- package/dist/signer/signer.d.ts +23 -30
- package/dist/signer/signer.d.ts.map +1 -1
- package/dist/signer/signer.js +0 -40
- package/dist/signer/signer.js.map +1 -1
- package/dist/token/token-claims.d.ts +81 -81
- package/dist/token/token-manager.d.ts +1 -2
- package/dist/token/token-manager.d.ts.map +1 -1
- package/dist/token/token-manager.js +10 -37
- package/dist/token/token-manager.js.map +1 -1
- package/dist/token/types.d.ts +10 -10
- package/package.json +2 -3
- package/src/account/account.ts +11 -7
- package/src/assets/app/backend-data.ts +9 -2
- package/src/assets/app/components/accept-form.tsx +65 -51
- package/src/assets/app/components/client-name.tsx +24 -16
- package/src/assets/app/components/url-viewer.tsx +3 -3
- package/src/assets/app/views/accept-view.tsx +7 -4
- package/src/assets/app/views/authorize-view.tsx +2 -1
- package/src/assets/assets-middleware.ts +14 -2
- package/src/client/client-manager.ts +78 -60
- package/src/client/client.ts +1 -4
- package/src/constants.ts +3 -0
- package/src/device/device-manager.ts +7 -1
- package/src/errors/invalid-authorization-details-error.ts +9 -4
- package/src/lib/http/request.ts +61 -15
- package/src/metadata/build-metadata.ts +9 -42
- package/src/oauth-hooks.ts +3 -13
- package/src/oauth-provider.ts +181 -159
- package/src/oauth-verifier.ts +1 -2
- package/src/output/build-authorize-data.ts +8 -0
- package/src/replay/replay-manager.ts +9 -0
- package/src/replay/replay-store.ts +1 -1
- package/src/request/request-info.ts +2 -0
- package/src/request/request-manager.ts +81 -107
- package/src/signer/signer.ts +0 -63
- package/src/token/token-manager.ts +8 -41
- package/dist/oidc/claims.d.ts +0 -16
- package/dist/oidc/claims.d.ts.map +0 -1
- package/dist/oidc/claims.js +0 -29
- package/dist/oidc/claims.js.map +0 -1
- package/dist/oidc/userinfo.d.ts +0 -7
- package/dist/oidc/userinfo.d.ts.map +0 -1
- package/dist/oidc/userinfo.js +0 -3
- package/dist/oidc/userinfo.js.map +0 -1
- package/dist/parameters/claims-requested.d.ts +0 -3
- package/dist/parameters/claims-requested.d.ts.map +0 -1
- package/dist/parameters/claims-requested.js +0 -77
- package/dist/parameters/claims-requested.js.map +0 -1
- package/dist/parameters/oidc-payload.d.ts +0 -31
- package/dist/parameters/oidc-payload.d.ts.map +0 -1
- package/dist/parameters/oidc-payload.js +0 -25
- package/dist/parameters/oidc-payload.js.map +0 -1
- package/src/assets/app/components/client-identifier.tsx +0 -31
- package/src/oidc/claims.ts +0 -35
- package/src/oidc/userinfo.ts +0 -11
- package/src/parameters/claims-requested.ts +0 -106
- package/src/parameters/oidc-payload.ts +0 -28
@@ -9,7 +9,7 @@ export interface ReplayStore {
|
|
9
9
|
* strictly necessary for security purposes, the namespace should be used to
|
10
10
|
* mitigate denial of service attacks from one client to the other.
|
11
11
|
*
|
12
|
-
* @param timeFrame expressed in milliseconds.
|
12
|
+
* @param timeFrame expressed in milliseconds.
|
13
13
|
*/
|
14
14
|
unique(
|
15
15
|
namespace: string,
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { OAuthAuthenticationRequestParameters } from '@atproto/oauth-types'
|
2
|
+
import { ClientId } from '../client/client-id.js'
|
2
3
|
import { ClientAuth } from '../client/client-auth.js'
|
3
4
|
import { RequestId } from './request-id.js'
|
4
5
|
import { RequestUri } from './request-uri.js'
|
@@ -8,5 +9,6 @@ export type RequestInfo = {
|
|
8
9
|
uri: RequestUri
|
9
10
|
parameters: Readonly<OAuthAuthenticationRequestParameters>
|
10
11
|
expiresAt: Date
|
12
|
+
clientId: ClientId
|
11
13
|
clientAuth: ClientAuth
|
12
14
|
}
|
@@ -4,7 +4,6 @@ import {
|
|
4
4
|
OAuthAuthorizationServerMetadata,
|
5
5
|
} from '@atproto/oauth-types'
|
6
6
|
|
7
|
-
import { DeviceAccountInfo } from '../account/account-store.js'
|
8
7
|
import { Account } from '../account/account.js'
|
9
8
|
import { ClientAuth } from '../client/client-auth.js'
|
10
9
|
import { ClientId } from '../client/client-id.js'
|
@@ -17,12 +16,12 @@ import {
|
|
17
16
|
import { DeviceId } from '../device/device-id.js'
|
18
17
|
import { AccessDeniedError } from '../errors/access-denied-error.js'
|
19
18
|
import { ConsentRequiredError } from '../errors/consent-required-error.js'
|
19
|
+
import { InvalidAuthorizationDetailsError } from '../errors/invalid-authorization-details-error.js'
|
20
20
|
import { InvalidGrantError } from '../errors/invalid-grant-error.js'
|
21
21
|
import { InvalidParametersError } from '../errors/invalid-parameters-error.js'
|
22
22
|
import { InvalidRequestError } from '../errors/invalid-request-error.js'
|
23
23
|
import { compareRedirectUri } from '../lib/util/redirect-uri.js'
|
24
24
|
import { OAuthHooks } from '../oauth-hooks.js'
|
25
|
-
import { OIDC_SCOPE_CLAIMS } from '../oidc/claims.js'
|
26
25
|
import { Signer } from '../signer/signer.js'
|
27
26
|
import { Code, generateCode } from './code.js'
|
28
27
|
import {
|
@@ -44,7 +43,6 @@ export class RequestManager {
|
|
44
43
|
protected readonly signer: Signer,
|
45
44
|
protected readonly metadata: OAuthAuthorizationServerMetadata,
|
46
45
|
protected readonly hooks: OAuthHooks,
|
47
|
-
protected readonly pkceRequired = true,
|
48
46
|
protected readonly tokenMaxAge = TOKEN_MAX_AGE,
|
49
47
|
) {}
|
50
48
|
|
@@ -83,7 +81,7 @@ export class RequestManager {
|
|
83
81
|
})
|
84
82
|
|
85
83
|
const uri = encodeRequestUri(id)
|
86
|
-
return { id, uri, expiresAt, parameters, clientAuth }
|
84
|
+
return { id, uri, expiresAt, parameters, clientId: client.id, clientAuth }
|
87
85
|
}
|
88
86
|
|
89
87
|
async validate(
|
@@ -91,8 +89,28 @@ export class RequestManager {
|
|
91
89
|
clientAuth: ClientAuth,
|
92
90
|
parameters: Readonly<OAuthAuthenticationRequestParameters>,
|
93
91
|
dpopJkt: null | string,
|
94
|
-
pkceRequired = this.pkceRequired,
|
95
92
|
): Promise<Readonly<OAuthAuthenticationRequestParameters>> {
|
93
|
+
for (const k of [
|
94
|
+
// Known unsupported OIDC parameters
|
95
|
+
'claims',
|
96
|
+
'id_token_hint',
|
97
|
+
'nonce', // note that OIDC "nonce" is redundant with PKCE
|
98
|
+
] as const) {
|
99
|
+
if (parameters[k]) {
|
100
|
+
throw new InvalidParametersError(
|
101
|
+
parameters,
|
102
|
+
`Unsupported "${k}" parameter`,
|
103
|
+
)
|
104
|
+
}
|
105
|
+
}
|
106
|
+
|
107
|
+
if (parameters.response_type !== 'code') {
|
108
|
+
throw new InvalidParametersError(
|
109
|
+
parameters,
|
110
|
+
'Only "code" response type is allowed',
|
111
|
+
)
|
112
|
+
}
|
113
|
+
|
96
114
|
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#section-1.4.1
|
97
115
|
// > The authorization server MAY fully or partially ignore the scope
|
98
116
|
// > requested by the client, based on the authorization server policy or
|
@@ -101,59 +119,75 @@ export class RequestManager {
|
|
101
119
|
// > server MUST include the scope response parameter in the token response
|
102
120
|
// > (Section 3.2.3) to inform the client of the actual scope granted.
|
103
121
|
|
104
|
-
const cScopes = client.metadata.scope?.split(' ')
|
122
|
+
const cScopes = client.metadata.scope?.split(' ').filter(Boolean)
|
105
123
|
const sScopes = this.metadata.scopes_supported
|
106
124
|
|
107
|
-
const scopes =
|
108
|
-
|
109
|
-
|
110
|
-
|
125
|
+
const scopes = new Set(
|
126
|
+
parameters.scope?.split(' ').filter(Boolean) || cScopes,
|
127
|
+
)
|
128
|
+
|
129
|
+
if (scopes.has('openid')) {
|
130
|
+
throw new InvalidParametersError(
|
131
|
+
parameters,
|
132
|
+
'OpenID Connect is not supported',
|
133
|
+
)
|
134
|
+
}
|
135
|
+
|
136
|
+
if (!scopes.has('atproto')) {
|
137
|
+
throw new InvalidParametersError(
|
138
|
+
parameters,
|
139
|
+
'The "atproto" scope is required',
|
140
|
+
)
|
141
|
+
}
|
111
142
|
|
112
143
|
for (const scope of scopes) {
|
113
|
-
|
144
|
+
// Loopback clients do not define any scope in their metadata
|
145
|
+
if (cScopes && !cScopes.includes(scope)) {
|
114
146
|
throw new InvalidParametersError(
|
115
147
|
parameters,
|
116
148
|
`Scope "${scope}" is not registered for this client`,
|
117
149
|
)
|
118
150
|
}
|
119
|
-
}
|
120
151
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
parameters,
|
130
|
-
`Essential ${claim} claim requires "${scope}" scope`,
|
131
|
-
)
|
132
|
-
}
|
133
|
-
}
|
152
|
+
// Currently, the implementation requires all the scopes to be statically
|
153
|
+
// defined in the server metadata. In the future, we might add support
|
154
|
+
// for dynamic scopes.
|
155
|
+
if (!sScopes?.includes(scope)) {
|
156
|
+
throw new InvalidParametersError(
|
157
|
+
parameters,
|
158
|
+
`Scope "${scope}" is not supported by this server`,
|
159
|
+
)
|
134
160
|
}
|
135
161
|
}
|
136
162
|
|
137
|
-
parameters = { ...parameters, scope: scopes.join(' ') }
|
138
|
-
|
139
|
-
const responseTypes = parameters.response_type.split(' ')
|
163
|
+
parameters = { ...parameters, scope: [...scopes].join(' ') || undefined }
|
140
164
|
|
141
165
|
if (parameters.authorization_details) {
|
142
166
|
const clientAuthDetailsTypes = client.metadata.authorization_details_types
|
143
167
|
if (!clientAuthDetailsTypes) {
|
144
|
-
throw new
|
168
|
+
throw new InvalidAuthorizationDetailsError(
|
145
169
|
parameters,
|
146
170
|
'Client Metadata does not declare any "authorization_details"',
|
147
171
|
)
|
148
172
|
}
|
149
173
|
|
150
174
|
for (const detail of parameters.authorization_details) {
|
151
|
-
if (
|
152
|
-
|
175
|
+
if (
|
176
|
+
!this.metadata.authorization_details_types_supported?.includes(
|
177
|
+
detail.type,
|
178
|
+
)
|
179
|
+
) {
|
180
|
+
throw new InvalidAuthorizationDetailsError(
|
153
181
|
parameters,
|
154
182
|
`Unsupported "authorization_details" type "${detail.type}"`,
|
155
183
|
)
|
156
184
|
}
|
185
|
+
if (!clientAuthDetailsTypes?.includes(detail.type)) {
|
186
|
+
throw new InvalidAuthorizationDetailsError(
|
187
|
+
parameters,
|
188
|
+
`Client Metadata does not declare any "authorization_details" of type "${detail.type}"`,
|
189
|
+
)
|
190
|
+
}
|
157
191
|
}
|
158
192
|
}
|
159
193
|
|
@@ -197,16 +231,9 @@ export class RequestManager {
|
|
197
231
|
)
|
198
232
|
}
|
199
233
|
|
200
|
-
if (pkceRequired && responseTypes.includes('token')) {
|
201
|
-
throw new InvalidParametersError(
|
202
|
-
parameters,
|
203
|
-
`Response type "${parameters.response_type}" is incompatible with PKCE`,
|
204
|
-
'unsupported_response_type',
|
205
|
-
)
|
206
|
-
}
|
207
|
-
|
208
234
|
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1
|
209
|
-
|
235
|
+
// PKCE is mandatory
|
236
|
+
if (!parameters.code_challenge) {
|
210
237
|
throw new InvalidParametersError(parameters, 'code_challenge is required')
|
211
238
|
}
|
212
239
|
|
@@ -229,50 +256,15 @@ export class RequestManager {
|
|
229
256
|
)
|
230
257
|
}
|
231
258
|
|
232
|
-
//
|
233
|
-
//
|
234
|
-
//
|
235
|
-
//
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
// > notes, see Section 15.5.2.
|
242
|
-
if (responseTypes.includes('id_token') && !parameters.nonce) {
|
243
|
-
throw new InvalidParametersError(
|
244
|
-
parameters,
|
245
|
-
'nonce is required for implicit and hybrid flows',
|
246
|
-
)
|
247
|
-
}
|
248
|
-
|
249
|
-
// Make "expensive" checks after the "cheaper" checks
|
250
|
-
|
251
|
-
if (parameters.id_token_hint != null) {
|
252
|
-
const { payload } = await this.signer.verify(parameters.id_token_hint, {
|
253
|
-
// these are meant to be outdated when used as a hint
|
254
|
-
clockTolerance: Infinity,
|
255
|
-
})
|
256
|
-
|
257
|
-
if (!payload.sub) {
|
258
|
-
throw new InvalidParametersError(
|
259
|
-
parameters,
|
260
|
-
`Unexpected empty id_token_hint "sub"`,
|
261
|
-
)
|
262
|
-
} else if (parameters.login_hint == null) {
|
263
|
-
parameters = { ...parameters, login_hint: payload.sub }
|
264
|
-
} else if (parameters.login_hint !== payload.sub) {
|
265
|
-
throw new InvalidParametersError(
|
266
|
-
parameters,
|
267
|
-
'login_hint does not match "sub" of id_token_hint',
|
268
|
-
)
|
269
|
-
}
|
270
|
-
}
|
271
|
-
|
272
|
-
// ATPROTO extension: if the client is not trusted, force users to consent
|
273
|
-
// to authorization requests. We do this to avoid unauthenticated clients
|
274
|
-
// from being able to silently re-authenticate users.
|
275
|
-
if (clientAuth.method === 'none' && !client.info.isFirstParty) {
|
259
|
+
// ATPROTO extension: if the client is not trusted, and not authenticated,
|
260
|
+
// force users to consent to authorization requests. We do this to avoid
|
261
|
+
// unauthenticated clients from being able to silently re-authenticate
|
262
|
+
// users.
|
263
|
+
if (
|
264
|
+
!client.info.isTrusted &&
|
265
|
+
!client.info.isFirstParty &&
|
266
|
+
clientAuth.method === 'none'
|
267
|
+
) {
|
276
268
|
if (parameters.prompt === 'none') {
|
277
269
|
throw new ConsentRequiredError(
|
278
270
|
parameters,
|
@@ -346,6 +338,7 @@ export class RequestManager {
|
|
346
338
|
uri,
|
347
339
|
expiresAt: updates.expiresAt || data.expiresAt,
|
348
340
|
parameters: data.parameters,
|
341
|
+
clientId: data.clientId,
|
349
342
|
clientAuth: data.clientAuth,
|
350
343
|
}
|
351
344
|
}
|
@@ -355,8 +348,7 @@ export class RequestManager {
|
|
355
348
|
uri: RequestUri,
|
356
349
|
deviceId: DeviceId,
|
357
350
|
account: Account,
|
358
|
-
|
359
|
-
): Promise<{ code?: Code; token?: string; id_token?: string }> {
|
351
|
+
): Promise<Code> {
|
360
352
|
const id = decodeRequestUri(uri)
|
361
353
|
|
362
354
|
const data = await this.store.readRequest(id)
|
@@ -385,18 +377,8 @@ export class RequestManager {
|
|
385
377
|
)
|
386
378
|
}
|
387
379
|
|
388
|
-
|
389
|
-
|
390
|
-
if (responseType.includes('token')) {
|
391
|
-
throw new AccessDeniedError(
|
392
|
-
data.parameters,
|
393
|
-
'Implicit "token" forbidden (use "code" with PKCE instead)',
|
394
|
-
)
|
395
|
-
}
|
396
|
-
|
397
|
-
const code = responseType.includes('code')
|
398
|
-
? await generateCode()
|
399
|
-
: undefined
|
380
|
+
// Only response_type=code is supported
|
381
|
+
const code = await generateCode()
|
400
382
|
|
401
383
|
// Bind the request to the account, preventing it from being used again.
|
402
384
|
await this.store.updateRequest(id, {
|
@@ -406,15 +388,7 @@ export class RequestManager {
|
|
406
388
|
expiresAt: new Date(Date.now() + AUTHORIZATION_INACTIVITY_TIMEOUT),
|
407
389
|
})
|
408
390
|
|
409
|
-
|
410
|
-
? await this.signer.idToken(client, data.parameters, account, {
|
411
|
-
auth_time: info.authenticatedAt,
|
412
|
-
exp: this.createTokenExpiry(),
|
413
|
-
code,
|
414
|
-
})
|
415
|
-
: undefined
|
416
|
-
|
417
|
-
return { code, id_token }
|
391
|
+
return code
|
418
392
|
} catch (err) {
|
419
393
|
await this.store.deleteRequest(id)
|
420
394
|
throw err
|
package/src/signer/signer.ts
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
import { randomBytes } from 'node:crypto'
|
2
|
-
|
3
1
|
import {
|
4
2
|
JwtPayload,
|
5
3
|
JwtPayloadGetter,
|
@@ -12,14 +10,10 @@ import {
|
|
12
10
|
OAuthAuthenticationRequestParameters,
|
13
11
|
OAuthAuthorizationDetails,
|
14
12
|
} from '@atproto/oauth-types'
|
15
|
-
import { generate as hash } from 'oidc-token-hash'
|
16
13
|
|
17
14
|
import { Account } from '../account/account.js'
|
18
15
|
import { Client } from '../client/client.js'
|
19
|
-
import { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'
|
20
16
|
import { dateToEpoch } from '../lib/util/date.js'
|
21
|
-
import { claimRequested } from '../parameters/claims-requested.js'
|
22
|
-
import { oidcPayload } from '../parameters/oidc-payload.js'
|
23
17
|
import { TokenId } from '../token/token-id.js'
|
24
18
|
import {
|
25
19
|
SignedTokenPayload,
|
@@ -105,61 +99,4 @@ export class Signer {
|
|
105
99
|
|
106
100
|
return result
|
107
101
|
}
|
108
|
-
|
109
|
-
async idToken(
|
110
|
-
client: Client,
|
111
|
-
params: OAuthAuthenticationRequestParameters,
|
112
|
-
account: Account,
|
113
|
-
extra: {
|
114
|
-
exp: Date
|
115
|
-
iat?: Date
|
116
|
-
auth_time?: Date
|
117
|
-
code?: string
|
118
|
-
access_token?: string
|
119
|
-
},
|
120
|
-
): Promise<SignedJwt> {
|
121
|
-
// This can happen when a client is using password_grant. If a client is
|
122
|
-
// using password_grant, it should not set "require_auth_time" to true.
|
123
|
-
if (client.metadata.require_auth_time && extra.auth_time == null) {
|
124
|
-
throw new InvalidClientMetadataError(
|
125
|
-
'"require_auth_time" metadata is not compatible with "password_grant" flow',
|
126
|
-
)
|
127
|
-
}
|
128
|
-
|
129
|
-
return this.sign(
|
130
|
-
{
|
131
|
-
alg: client.metadata.id_token_signed_response_alg,
|
132
|
-
typ: 'JWT',
|
133
|
-
},
|
134
|
-
async ({ alg }, key) => ({
|
135
|
-
...oidcPayload(params, account),
|
136
|
-
|
137
|
-
aud: client.id,
|
138
|
-
iat: dateToEpoch(extra.iat),
|
139
|
-
exp: dateToEpoch(extra.exp),
|
140
|
-
sub: account.sub,
|
141
|
-
jti: randomBytes(16).toString('hex'),
|
142
|
-
scope: params.scope,
|
143
|
-
nonce: params.nonce,
|
144
|
-
|
145
|
-
s_hash: params.state //
|
146
|
-
? await hash(params.state, alg, key.crv)
|
147
|
-
: undefined,
|
148
|
-
c_hash: extra.code //
|
149
|
-
? await hash(extra.code, alg, key.crv)
|
150
|
-
: undefined,
|
151
|
-
at_hash: extra.access_token //
|
152
|
-
? await hash(extra.access_token, alg, key.crv)
|
153
|
-
: undefined,
|
154
|
-
|
155
|
-
// https://openid.net/specs/openid-provider-authentication-policy-extension-1_0.html#rfc.section.5.2
|
156
|
-
auth_time:
|
157
|
-
client.metadata.require_auth_time ||
|
158
|
-
(extra.auth_time != null && params.max_age != null) ||
|
159
|
-
claimRequested(params, 'id_token', 'auth_time', extra.auth_time)
|
160
|
-
? dateToEpoch(extra.auth_time!)
|
161
|
-
: undefined,
|
162
|
-
}),
|
163
|
-
)
|
164
|
-
}
|
165
102
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { isSignedJwt
|
1
|
+
import { isSignedJwt } from '@atproto/jwk'
|
2
2
|
import {
|
3
3
|
AccessToken,
|
4
4
|
CLIENT_ASSERTION_TYPE_JWT_BEARER,
|
@@ -140,6 +140,10 @@ export class TokenManager {
|
|
140
140
|
if (!('code_verifier' in input) || !input.code_verifier) {
|
141
141
|
throw new InvalidGrantError('code_verifier is required')
|
142
142
|
}
|
143
|
+
// Prevent client from generating too short code_verifiers
|
144
|
+
if (input.code_verifier.length < 43) {
|
145
|
+
throw new InvalidGrantError('code_verifier too short')
|
146
|
+
}
|
143
147
|
switch (parameters.code_challenge_method) {
|
144
148
|
case undefined: // Default is "plain" (per spec)
|
145
149
|
case 'plain': {
|
@@ -181,8 +185,7 @@ export class TokenManager {
|
|
181
185
|
}
|
182
186
|
|
183
187
|
const tokenId = await generateTokenId()
|
184
|
-
const
|
185
|
-
const refreshToken = scopes?.includes('offline_access')
|
188
|
+
const refreshToken = client.metadata.grant_types.includes('refresh_token')
|
186
189
|
? await generateRefreshToken()
|
187
190
|
: undefined
|
188
191
|
|
@@ -222,22 +225,10 @@ export class TokenManager {
|
|
222
225
|
authorization_details: authorizationDetails,
|
223
226
|
})
|
224
227
|
|
225
|
-
const idToken = scopes?.includes('openid')
|
226
|
-
? await this.signer.idToken(client, parameters, account, {
|
227
|
-
exp: expiresAt,
|
228
|
-
iat: now,
|
229
|
-
// If there is no deviceInfo, we are in a "password_grant" context
|
230
|
-
auth_time: device?.info.authenticatedAt || new Date(),
|
231
|
-
access_token: accessToken,
|
232
|
-
code,
|
233
|
-
})
|
234
|
-
: undefined
|
235
|
-
|
236
228
|
return this.buildTokenResponse(
|
237
229
|
client,
|
238
230
|
accessToken,
|
239
231
|
refreshToken,
|
240
|
-
idToken,
|
241
232
|
expiresAt,
|
242
233
|
parameters,
|
243
234
|
account,
|
@@ -249,7 +240,6 @@ export class TokenManager {
|
|
249
240
|
client: Client,
|
250
241
|
accessToken: AccessToken,
|
251
242
|
refreshToken: string | undefined,
|
252
|
-
idToken: SignedJwt | undefined,
|
253
243
|
expiresAt: Date,
|
254
244
|
parameters: OAuthAuthenticationRequestParameters,
|
255
245
|
account: Account,
|
@@ -259,8 +249,7 @@ export class TokenManager {
|
|
259
249
|
access_token: accessToken,
|
260
250
|
token_type: parameters.dpop_jkt ? 'DPoP' : 'Bearer',
|
261
251
|
refresh_token: refreshToken,
|
262
|
-
|
263
|
-
scope: parameters.scope ?? '',
|
252
|
+
scope: parameters.scope,
|
264
253
|
authorization_details: authorizationDetails,
|
265
254
|
get expires_in() {
|
266
255
|
return dateToRelativeSeconds(expiresAt)
|
@@ -272,12 +261,6 @@ export class TokenManager {
|
|
272
261
|
sub: account.sub,
|
273
262
|
}
|
274
263
|
|
275
|
-
await this.hooks.onTokenResponse?.call(null, tokenResponse, {
|
276
|
-
client,
|
277
|
-
parameters,
|
278
|
-
account,
|
279
|
-
})
|
280
|
-
|
281
264
|
return tokenResponse
|
282
265
|
}
|
283
266
|
|
@@ -316,7 +299,7 @@ export class TokenManager {
|
|
316
299
|
throw new InvalidGrantError(`Invalid refresh token`)
|
317
300
|
}
|
318
301
|
|
319
|
-
const { account,
|
302
|
+
const { account, data } = tokenInfo
|
320
303
|
const { parameters } = data
|
321
304
|
|
322
305
|
try {
|
@@ -400,26 +383,10 @@ export class TokenManager {
|
|
400
383
|
authorization_details,
|
401
384
|
})
|
402
385
|
|
403
|
-
// https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.3.3
|
404
|
-
//
|
405
|
-
// > In addition to the response parameters specified by OAuth 2.0, the
|
406
|
-
// > following parameters MUST be included in the response:
|
407
|
-
// > - id_token: ID Token value associated with the authenticated session.
|
408
|
-
const scopes = parameters.scope?.split(' ')
|
409
|
-
const idToken = scopes?.includes('openid')
|
410
|
-
? await this.signer.idToken(client, parameters, account, {
|
411
|
-
exp: expiresAt,
|
412
|
-
iat: now,
|
413
|
-
auth_time: info?.authenticatedAt,
|
414
|
-
access_token: accessToken,
|
415
|
-
})
|
416
|
-
: undefined
|
417
|
-
|
418
386
|
return this.buildTokenResponse(
|
419
387
|
client,
|
420
388
|
accessToken,
|
421
389
|
nextRefreshToken,
|
422
|
-
idToken,
|
423
390
|
expiresAt,
|
424
391
|
parameters,
|
425
392
|
account,
|
package/dist/oidc/claims.d.ts
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
import { JwtPayload } from '@atproto/jwk';
|
2
|
-
/**
|
3
|
-
* @see {@link https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims | OpenID Connect Core 1.0, 5.4. Requesting Claims using Scope Values}
|
4
|
-
*/
|
5
|
-
export declare const OIDC_SCOPE_CLAIMS: Readonly<{
|
6
|
-
email: readonly ["email", "email_verified"];
|
7
|
-
phone: readonly ["phone_number", "phone_number_verified"];
|
8
|
-
address: readonly ["address"];
|
9
|
-
profile: readonly ["name", "family_name", "given_name", "middle_name", "nickname", "preferred_username", "gender", "picture", "profile", "website", "birthdate", "zoneinfo", "locale", "updated_at"];
|
10
|
-
}>;
|
11
|
-
export declare const OIDC_STANDARD_CLAIMS: readonly ("name" | "email" | "email_verified" | "phone_number" | "phone_number_verified" | "address" | "profile" | "family_name" | "given_name" | "middle_name" | "nickname" | "preferred_username" | "gender" | "picture" | "website" | "birthdate" | "zoneinfo" | "locale" | "updated_at")[];
|
12
|
-
export type OIDCStandardClaim = (typeof OIDC_STANDARD_CLAIMS)[number];
|
13
|
-
export type OIDCStandardPayload = Partial<{
|
14
|
-
[K in OIDCStandardClaim]?: JwtPayload[K];
|
15
|
-
}>;
|
16
|
-
//# sourceMappingURL=claims.d.ts.map
|
@@ -1 +0,0 @@
|
|
1
|
-
{"version":3,"file":"claims.d.ts","sourceRoot":"","sources":["../../src/oidc/claims.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEzC;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;EAoB5B,CAAA;AAEF,eAAO,MAAM,oBAAoB,gSAEhC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAA;AACrE,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC;KACvC,CAAC,IAAI,iBAAiB,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;CACzC,CAAC,CAAA"}
|
package/dist/oidc/claims.js
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
"use strict";
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.OIDC_STANDARD_CLAIMS = exports.OIDC_SCOPE_CLAIMS = void 0;
|
4
|
-
/**
|
5
|
-
* @see {@link https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims | OpenID Connect Core 1.0, 5.4. Requesting Claims using Scope Values}
|
6
|
-
*/
|
7
|
-
exports.OIDC_SCOPE_CLAIMS = Object.freeze({
|
8
|
-
email: Object.freeze(['email', 'email_verified']),
|
9
|
-
phone: Object.freeze(['phone_number', 'phone_number_verified']),
|
10
|
-
address: Object.freeze(['address']),
|
11
|
-
profile: Object.freeze([
|
12
|
-
'name',
|
13
|
-
'family_name',
|
14
|
-
'given_name',
|
15
|
-
'middle_name',
|
16
|
-
'nickname',
|
17
|
-
'preferred_username',
|
18
|
-
'gender',
|
19
|
-
'picture',
|
20
|
-
'profile',
|
21
|
-
'website',
|
22
|
-
'birthdate',
|
23
|
-
'zoneinfo',
|
24
|
-
'locale',
|
25
|
-
'updated_at',
|
26
|
-
]),
|
27
|
-
});
|
28
|
-
exports.OIDC_STANDARD_CLAIMS = Object.freeze(Object.values(exports.OIDC_SCOPE_CLAIMS).flat());
|
29
|
-
//# sourceMappingURL=claims.js.map
|
package/dist/oidc/claims.js.map
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
{"version":3,"file":"claims.js","sourceRoot":"","sources":["../../src/oidc/claims.ts"],"names":[],"mappings":";;;AAEA;;GAEG;AACU,QAAA,iBAAiB,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7C,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,gBAAgB,CAAU,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,EAAE,uBAAuB,CAAU,CAAC;IACxE,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAU,CAAC;IAC5C,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC;QACrB,MAAM;QACN,aAAa;QACb,YAAY;QACZ,aAAa;QACb,UAAU;QACV,oBAAoB;QACpB,QAAQ;QACR,SAAS;QACT,SAAS;QACT,SAAS;QACT,WAAW;QACX,UAAU;QACV,QAAQ;QACR,YAAY;KACJ,CAAC;CACZ,CAAC,CAAA;AAEW,QAAA,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAC/C,MAAM,CAAC,MAAM,CAAC,yBAAiB,CAAC,CAAC,IAAI,EAAE,CACxC,CAAA"}
|
package/dist/oidc/userinfo.d.ts
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
{"version":3,"file":"userinfo.d.ts","sourceRoot":"","sources":["../../src/oidc/userinfo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAEjD,MAAM,MAAM,QAAQ,GAAG,mBAAmB,GAAG;IAE3C,GAAG,EAAE,MAAM,CAAA;IAGX,SAAS,EAAE,MAAM,CAAA;IAEjB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA"}
|
package/dist/oidc/userinfo.js
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
{"version":3,"file":"userinfo.js","sourceRoot":"","sources":["../../src/oidc/userinfo.ts"],"names":[],"mappings":""}
|
@@ -1,3 +0,0 @@
|
|
1
|
-
import { OAuthAuthenticationRequestParameters, OidcClaimsParameter, OidcEntityType } from '@atproto/oauth-types';
|
2
|
-
export declare function claimRequested(parameters: OAuthAuthenticationRequestParameters, entityType: OidcEntityType, claimName: OidcClaimsParameter, value: unknown): boolean;
|
3
|
-
//# sourceMappingURL=claims-requested.d.ts.map
|
@@ -1 +0,0 @@
|
|
1
|
-
{"version":3,"file":"claims-requested.d.ts","sourceRoot":"","sources":["../../src/parameters/claims-requested.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oCAAoC,EACpC,mBAAmB,EACnB,cAAc,EACf,MAAM,sBAAsB,CAAA;AAG7B,wBAAgB,cAAc,CAC5B,UAAU,EAAE,oCAAoC,EAChD,UAAU,EAAE,cAAc,EAC1B,SAAS,EAAE,mBAAmB,EAC9B,KAAK,EAAE,OAAO,GACb,OAAO,CA+BT"}
|