@atproto/oauth-provider 0.1.3 → 0.2.0
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/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
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
isLoopbackUrl,
|
|
18
18
|
isOAuthClientIdDiscoverable,
|
|
19
19
|
isOAuthClientIdLoopback,
|
|
20
|
+
OAuthAuthorizationServerMetadata,
|
|
20
21
|
OAuthClientIdDiscoverable,
|
|
21
22
|
OAuthClientIdLoopback,
|
|
22
23
|
OAuthClientMetadata,
|
|
@@ -55,9 +56,10 @@ export type LoopbackMetadataGetter = (
|
|
|
55
56
|
|
|
56
57
|
export class ClientManager {
|
|
57
58
|
protected readonly jwks: CachedGetter<string, Jwks>
|
|
58
|
-
protected readonly
|
|
59
|
+
protected readonly metadataGetter: CachedGetter<string, OAuthClientMetadata>
|
|
59
60
|
|
|
60
61
|
constructor(
|
|
62
|
+
protected readonly serverMetadata: OAuthAuthorizationServerMetadata,
|
|
61
63
|
protected readonly keyset: Keyset,
|
|
62
64
|
protected readonly hooks: OAuthHooks,
|
|
63
65
|
protected readonly store: ClientStore | null,
|
|
@@ -76,7 +78,7 @@ export class ClientManager {
|
|
|
76
78
|
return jwks
|
|
77
79
|
}, clientJwksCache)
|
|
78
80
|
|
|
79
|
-
this.
|
|
81
|
+
this.metadataGetter = new CachedGetter(async (uri, options) => {
|
|
80
82
|
const metadata = await fetch(buildJsonGetRequest(uri, options)).then(
|
|
81
83
|
fetchMetadataHandler,
|
|
82
84
|
)
|
|
@@ -159,7 +161,7 @@ export class ClientManager {
|
|
|
159
161
|
): Promise<OAuthClientMetadata> {
|
|
160
162
|
const metadataUrl = parseDiscoverableClientId(clientId)
|
|
161
163
|
|
|
162
|
-
const metadata = await this.
|
|
164
|
+
const metadata = await this.metadataGetter.get(metadataUrl.href)
|
|
163
165
|
|
|
164
166
|
// Note: we do *not* re-validate the metadata here, as the metadata is
|
|
165
167
|
// validated within the getter. This is to avoid double validation.
|
|
@@ -195,6 +197,18 @@ export class ClientManager {
|
|
|
195
197
|
)
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
// Known OIDC specific parameters
|
|
201
|
+
for (const k of [
|
|
202
|
+
'default_max_age',
|
|
203
|
+
'userinfo_signed_response_alg',
|
|
204
|
+
'id_token_signed_response_alg',
|
|
205
|
+
'userinfo_encrypted_response_alg',
|
|
206
|
+
] as const) {
|
|
207
|
+
if (metadata[k] != null) {
|
|
208
|
+
throw new InvalidClientMetadataError(`Unsupported "${k}" parameter`)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
198
212
|
const clientUriUrl = metadata.client_uri
|
|
199
213
|
? new URL(metadata.client_uri)
|
|
200
214
|
: null
|
|
@@ -204,13 +218,27 @@ export class ClientManager {
|
|
|
204
218
|
throw new InvalidClientMetadataError('client_uri must be a valid URL')
|
|
205
219
|
}
|
|
206
220
|
|
|
207
|
-
const scopes = metadata.scope?.split(' ')
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
221
|
+
const scopes = metadata.scope?.split(' ').filter(Boolean)
|
|
222
|
+
|
|
223
|
+
const dupScope = scopes?.find(isDuplicate)
|
|
224
|
+
if (dupScope) {
|
|
225
|
+
throw new InvalidClientMetadataError(`Duplicate scope "${dupScope}"`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (scopes) {
|
|
229
|
+
for (const scope of scopes) {
|
|
230
|
+
// Note, once we have dynamic scopes, this check will need to be
|
|
231
|
+
// updated to check against the server's supported scopes.
|
|
232
|
+
if (!this.serverMetadata.scopes_supported?.includes(scope)) {
|
|
233
|
+
throw new InvalidClientMetadataError(`Unsupported scope "${scope}"`)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const dupGrantType = metadata.grant_types.find(isDuplicate)
|
|
239
|
+
if (dupGrantType) {
|
|
212
240
|
throw new InvalidClientMetadataError(
|
|
213
|
-
|
|
241
|
+
`Duplicate grant type "${dupGrantType}"`,
|
|
214
242
|
)
|
|
215
243
|
}
|
|
216
244
|
|
|
@@ -218,8 +246,8 @@ export class ClientManager {
|
|
|
218
246
|
switch (grantType) {
|
|
219
247
|
case 'authorization_code':
|
|
220
248
|
case 'refresh_token':
|
|
221
|
-
case 'implicit': // Required by OIDC (for id_token)
|
|
222
249
|
continue
|
|
250
|
+
case 'implicit':
|
|
223
251
|
case 'password':
|
|
224
252
|
throw new InvalidClientMetadataError(
|
|
225
253
|
`Grant type "${grantType}" is not allowed`,
|
|
@@ -241,35 +269,6 @@ export class ClientManager {
|
|
|
241
269
|
)
|
|
242
270
|
}
|
|
243
271
|
|
|
244
|
-
if (
|
|
245
|
-
metadata.userinfo_signed_response_alg &&
|
|
246
|
-
!this.keyset.signAlgorithms.includes(
|
|
247
|
-
metadata.userinfo_signed_response_alg,
|
|
248
|
-
)
|
|
249
|
-
) {
|
|
250
|
-
throw new InvalidClientMetadataError(
|
|
251
|
-
`Unsupported "userinfo_signed_response_alg" ${metadata.userinfo_signed_response_alg}`,
|
|
252
|
-
)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
metadata.id_token_signed_response_alg &&
|
|
257
|
-
!this.keyset.signAlgorithms.includes(
|
|
258
|
-
metadata.id_token_signed_response_alg,
|
|
259
|
-
)
|
|
260
|
-
) {
|
|
261
|
-
throw new InvalidClientMetadataError(
|
|
262
|
-
`Unsupported "id_token_signed_response_alg" ${metadata.id_token_signed_response_alg}`,
|
|
263
|
-
)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (metadata.userinfo_encrypted_response_alg) {
|
|
267
|
-
// We only support signature for now.
|
|
268
|
-
throw new InvalidClientMetadataError(
|
|
269
|
-
'Encrypted userinfo response is not supported',
|
|
270
|
-
)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
272
|
const method = metadata[`token_endpoint_auth_method`]
|
|
274
273
|
switch (method) {
|
|
275
274
|
case undefined:
|
|
@@ -338,37 +337,28 @@ export class ClientManager {
|
|
|
338
337
|
}
|
|
339
338
|
|
|
340
339
|
for (const responseType of metadata.response_types) {
|
|
341
|
-
|
|
340
|
+
if (responseType.includes('id_token')) {
|
|
341
|
+
throw new InvalidClientMetadataError(
|
|
342
|
+
`OpenID Connect response type "${responseType}" is not supported`,
|
|
343
|
+
)
|
|
344
|
+
}
|
|
342
345
|
|
|
343
346
|
// ATPROTO spec requires the use of PKCE
|
|
344
|
-
if (
|
|
347
|
+
if (responseType !== 'code') {
|
|
345
348
|
throw new InvalidClientMetadataError(
|
|
346
|
-
|
|
349
|
+
`Unsupported response type "${responseType}"`,
|
|
347
350
|
)
|
|
348
351
|
}
|
|
349
352
|
|
|
350
353
|
// Consistency check
|
|
351
354
|
if (
|
|
352
|
-
|
|
355
|
+
responseType === 'code' &&
|
|
353
356
|
!metadata.grant_types.includes('authorization_code')
|
|
354
357
|
) {
|
|
355
358
|
throw new InvalidClientMetadataError(
|
|
356
359
|
`Response type "${responseType}" requires the "authorization_code" grant type`,
|
|
357
360
|
)
|
|
358
361
|
}
|
|
359
|
-
|
|
360
|
-
// Asking for "code token" or "code id_token" is fine (as long as the
|
|
361
|
-
// grant_types includes "authorization_code" and the scope includes
|
|
362
|
-
// "openid"). Asking for "token" or "id_token" (without "code") requires
|
|
363
|
-
// the "implicit" grant type.
|
|
364
|
-
if (
|
|
365
|
-
(rt.includes('token') || rt.includes('id_token')) &&
|
|
366
|
-
!metadata.grant_types.includes('implicit')
|
|
367
|
-
) {
|
|
368
|
-
throw new InvalidClientMetadataError(
|
|
369
|
-
`Response type "${responseType}" requires the "implicit" grant type`,
|
|
370
|
-
)
|
|
371
|
-
}
|
|
372
362
|
}
|
|
373
363
|
|
|
374
364
|
if (metadata.application_type === 'native') {
|
|
@@ -383,11 +373,33 @@ export class ClientManager {
|
|
|
383
373
|
// > accordingly.
|
|
384
374
|
}
|
|
385
375
|
|
|
376
|
+
if (metadata.authorization_details_types?.length) {
|
|
377
|
+
const dupAuthDetailsType =
|
|
378
|
+
metadata.authorization_details_types.find(isDuplicate)
|
|
379
|
+
if (dupAuthDetailsType) {
|
|
380
|
+
throw new InvalidClientMetadataError(
|
|
381
|
+
`Duplicate authorization_details_type "${dupAuthDetailsType}"`,
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const authorizationDetailsTypesSupported =
|
|
386
|
+
this.serverMetadata.authorization_details_types_supported
|
|
387
|
+
if (!authorizationDetailsTypesSupported) {
|
|
388
|
+
throw new InvalidClientMetadataError(
|
|
389
|
+
'authorization_details_types are not supported',
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
for (const type of metadata.authorization_details_types) {
|
|
393
|
+
if (!authorizationDetailsTypesSupported.includes(type)) {
|
|
394
|
+
throw new InvalidClientMetadataError(
|
|
395
|
+
`Unsupported authorization_details_type "${type}"`,
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
386
401
|
if (!metadata.redirect_uris?.length) {
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
// > OPs can require that request_uri values used be pre-registered with
|
|
390
|
-
// > the require_request_uri_registration discovery parameter.
|
|
402
|
+
// ATPROTO spec requires that at least one redirect URI is provided
|
|
391
403
|
|
|
392
404
|
throw new InvalidClientMetadataError(
|
|
393
405
|
'At least one redirect_uri is required',
|
|
@@ -786,6 +798,12 @@ export class ClientManager {
|
|
|
786
798
|
}
|
|
787
799
|
}
|
|
788
800
|
|
|
801
|
+
function isDuplicate<
|
|
802
|
+
T extends string | number | boolean | null | undefined | symbol,
|
|
803
|
+
>(value: T, index: number, array: T[]) {
|
|
804
|
+
return array.includes(value, index + 1)
|
|
805
|
+
}
|
|
806
|
+
|
|
789
807
|
function reverseDomain(domain: string) {
|
|
790
808
|
return domain.split('.').reverse().join('.')
|
|
791
809
|
}
|
package/src/client/client.ts
CHANGED
|
@@ -140,6 +140,7 @@ export class Client {
|
|
|
140
140
|
audience: checks.audience,
|
|
141
141
|
subject: this.id,
|
|
142
142
|
maxTokenAge: CLIENT_ASSERTION_MAX_AGE / 1000,
|
|
143
|
+
requiredClaims: ['jti'],
|
|
143
144
|
}).catch((err) => {
|
|
144
145
|
if (err instanceof JOSEError) {
|
|
145
146
|
const msg = `Validation of "client_assertion" failed: ${err.message}`
|
|
@@ -153,10 +154,6 @@ export class Client {
|
|
|
153
154
|
throw new InvalidClientError(`"kid" required in client_assertion`)
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
if (!result.payload.jti) {
|
|
157
|
-
throw new InvalidClientError(`"jti" required in client_assertion`)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
157
|
const clientAuth: ClientAuth = {
|
|
161
158
|
method: CLIENT_ASSERTION_TYPE_JWT_BEARER,
|
|
162
159
|
jkt: await authJwkThumbprint(result.key),
|
package/src/constants.ts
CHANGED
|
@@ -100,10 +100,16 @@ export class DeviceManager {
|
|
|
100
100
|
public async load(
|
|
101
101
|
req: IncomingMessage,
|
|
102
102
|
res: ServerResponse,
|
|
103
|
+
forceRotate = false,
|
|
103
104
|
): Promise<{ deviceId: DeviceId }> {
|
|
104
105
|
const cookie = await this.getCookie(req)
|
|
105
106
|
if (cookie) {
|
|
106
|
-
return this.refresh(
|
|
107
|
+
return this.refresh(
|
|
108
|
+
req,
|
|
109
|
+
res,
|
|
110
|
+
cookie.value,
|
|
111
|
+
forceRotate || cookie.mustRotate,
|
|
112
|
+
)
|
|
107
113
|
} else {
|
|
108
114
|
return this.create(req, res)
|
|
109
115
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OAuthAuthenticationRequestParameters } from '@atproto/oauth-types'
|
|
2
|
+
import { AccessDeniedError } from './access-denied-error.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* @see
|
|
@@ -15,8 +16,12 @@ import { OAuthError } from './oauth-error.js'
|
|
|
15
16
|
* - contains fields with invalid values for the authorization details type, or
|
|
16
17
|
* - is missing required fields for the authorization details type.
|
|
17
18
|
*/
|
|
18
|
-
export class InvalidAuthorizationDetailsError extends
|
|
19
|
-
constructor(
|
|
20
|
-
|
|
19
|
+
export class InvalidAuthorizationDetailsError extends AccessDeniedError {
|
|
20
|
+
constructor(
|
|
21
|
+
parameters: OAuthAuthenticationRequestParameters,
|
|
22
|
+
error_description: string,
|
|
23
|
+
cause?: unknown,
|
|
24
|
+
) {
|
|
25
|
+
super(parameters, error_description, 'invalid_authorization_details', cause)
|
|
21
26
|
}
|
|
22
27
|
}
|
package/src/lib/http/request.ts
CHANGED
|
@@ -28,6 +28,27 @@ export async function validateRequestPayload<S extends z.ZodTypeAny>(
|
|
|
28
28
|
return schema.parseAsync(payload, { path: ['body'] })
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export function validateHeaderValue(
|
|
32
|
+
req: IncomingMessage,
|
|
33
|
+
name: keyof IncomingMessage['headers'],
|
|
34
|
+
allowedValues: readonly (string | null)[],
|
|
35
|
+
) {
|
|
36
|
+
const value = req.headers[name] ?? null
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
throw createHttpError(400, `Invalid ${name} header`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!allowedValues.includes(value)) {
|
|
43
|
+
throw createHttpError(
|
|
44
|
+
400,
|
|
45
|
+
value
|
|
46
|
+
? `Forbidden ${name} header "${value}" (expected ${allowedValues})`
|
|
47
|
+
: `Missing ${name} header`,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
export function validateFetchMode(
|
|
32
53
|
req: IncomingMessage,
|
|
33
54
|
res: ServerResponse,
|
|
@@ -39,20 +60,45 @@ export function validateFetchMode(
|
|
|
39
60
|
| 'cors'
|
|
40
61
|
)[],
|
|
41
62
|
) {
|
|
42
|
-
|
|
63
|
+
validateHeaderValue(req, 'sec-fetch-mode', expectedMode)
|
|
64
|
+
}
|
|
43
65
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
export function validateFetchDest(
|
|
67
|
+
req: IncomingMessage,
|
|
68
|
+
res: ServerResponse,
|
|
69
|
+
expectedDest: readonly (
|
|
70
|
+
| null
|
|
71
|
+
| 'document'
|
|
72
|
+
| 'embed'
|
|
73
|
+
| 'font'
|
|
74
|
+
| 'image'
|
|
75
|
+
| 'manifest'
|
|
76
|
+
| 'media'
|
|
77
|
+
| 'object'
|
|
78
|
+
| 'report'
|
|
79
|
+
| 'script'
|
|
80
|
+
| 'serviceworker'
|
|
81
|
+
| 'sharedworker'
|
|
82
|
+
| 'style'
|
|
83
|
+
| 'worker'
|
|
84
|
+
| 'xslt'
|
|
85
|
+
)[],
|
|
86
|
+
) {
|
|
87
|
+
validateHeaderValue(req, 'sec-fetch-dest', expectedDest)
|
|
88
|
+
}
|
|
47
89
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
90
|
+
export function validateFetchSite(
|
|
91
|
+
req: IncomingMessage,
|
|
92
|
+
res: ServerResponse,
|
|
93
|
+
expectedSite: readonly (
|
|
94
|
+
| null
|
|
95
|
+
| 'same-origin'
|
|
96
|
+
| 'same-site'
|
|
97
|
+
| 'cross-site'
|
|
98
|
+
| 'none'
|
|
99
|
+
)[],
|
|
100
|
+
) {
|
|
101
|
+
validateHeaderValue(req, 'sec-fetch-site', expectedSite)
|
|
56
102
|
}
|
|
57
103
|
|
|
58
104
|
export function validateReferer(
|
|
@@ -64,7 +110,7 @@ export function validateReferer(
|
|
|
64
110
|
const referer = req.headers['referer']
|
|
65
111
|
const refererUrl = referer ? new URL(referer) : null
|
|
66
112
|
if (refererUrl ? !urlMatch(refererUrl, reference) : !allowNull) {
|
|
67
|
-
throw createHttpError(
|
|
113
|
+
throw createHttpError(400, `Invalid referer ${referer}`)
|
|
68
114
|
}
|
|
69
115
|
}
|
|
70
116
|
|
|
@@ -95,7 +141,7 @@ export function validateSameOrigin(
|
|
|
95
141
|
) {
|
|
96
142
|
const reqOrigin = req.headers['origin']
|
|
97
143
|
if (reqOrigin ? reqOrigin !== origin : !allowNull) {
|
|
98
|
-
throw createHttpError(
|
|
144
|
+
throw createHttpError(400, `Invalid origin ${reqOrigin}`)
|
|
99
145
|
}
|
|
100
146
|
}
|
|
101
147
|
|
|
@@ -113,7 +159,7 @@ export function validateCsrfToken(
|
|
|
113
159
|
!cookieName ||
|
|
114
160
|
cookies[cookieName] !== csrfToken
|
|
115
161
|
) {
|
|
116
|
-
throw createHttpError(
|
|
162
|
+
throw createHttpError(400, `Invalid CSRF token`)
|
|
117
163
|
}
|
|
118
164
|
|
|
119
165
|
if (clearCookie) {
|
|
@@ -2,11 +2,9 @@ import { Keyset } from '@atproto/jwk'
|
|
|
2
2
|
import { OAuthAuthorizationServerMetadata } from '@atproto/oauth-types'
|
|
3
3
|
|
|
4
4
|
import { Client } from '../client/client.js'
|
|
5
|
-
import { OIDC_STANDARD_CLAIMS } from '../oidc/claims.js'
|
|
6
5
|
import { VERIFY_ALGOS } from '../lib/util/crypto.js'
|
|
7
6
|
|
|
8
7
|
export type CustomMetadata = {
|
|
9
|
-
claims_supported?: string[]
|
|
10
8
|
scopes_supported?: string[]
|
|
11
9
|
authorization_details_types_supported?: string[]
|
|
12
10
|
protected_resources?: string[]
|
|
@@ -25,35 +23,10 @@ export function buildMetadata(
|
|
|
25
23
|
issuer,
|
|
26
24
|
|
|
27
25
|
scopes_supported: [
|
|
28
|
-
'
|
|
29
|
-
|
|
30
|
-
'email',
|
|
31
|
-
'phone',
|
|
32
|
-
'profile',
|
|
33
|
-
|
|
26
|
+
'atproto',
|
|
27
|
+
//
|
|
34
28
|
...(customMetadata?.scopes_supported ?? []),
|
|
35
29
|
],
|
|
36
|
-
claims_supported: [
|
|
37
|
-
/* IESG (Always provided) */
|
|
38
|
-
|
|
39
|
-
'sub', // did
|
|
40
|
-
'iss', // Authorization Server Origin
|
|
41
|
-
'aud',
|
|
42
|
-
'exp',
|
|
43
|
-
'iat',
|
|
44
|
-
'jti',
|
|
45
|
-
'client_id',
|
|
46
|
-
|
|
47
|
-
/* OpenID */
|
|
48
|
-
|
|
49
|
-
// 'acr', // "0"
|
|
50
|
-
// 'amr',
|
|
51
|
-
// 'azp',
|
|
52
|
-
'auth_time', // number - seconds since epoch
|
|
53
|
-
'nonce', // always required in "id_token", why would it not be supported?
|
|
54
|
-
|
|
55
|
-
...(customMetadata?.claims_supported ?? OIDC_STANDARD_CLAIMS),
|
|
56
|
-
],
|
|
57
30
|
subject_types_supported: [
|
|
58
31
|
//
|
|
59
32
|
'public', // The same "sub" is returned for all clients
|
|
@@ -62,15 +35,15 @@ export function buildMetadata(
|
|
|
62
35
|
response_types_supported: [
|
|
63
36
|
// OAuth
|
|
64
37
|
'code',
|
|
65
|
-
'token',
|
|
38
|
+
// 'token',
|
|
66
39
|
|
|
67
40
|
// OpenID
|
|
68
|
-
'none',
|
|
69
|
-
'code id_token token',
|
|
70
|
-
'code id_token',
|
|
71
|
-
'code token',
|
|
72
|
-
'id_token token',
|
|
73
|
-
'id_token',
|
|
41
|
+
// 'none',
|
|
42
|
+
// 'code id_token token',
|
|
43
|
+
// 'code id_token',
|
|
44
|
+
// 'code token',
|
|
45
|
+
// 'id_token token',
|
|
46
|
+
// 'id_token',
|
|
74
47
|
],
|
|
75
48
|
response_modes_supported: [
|
|
76
49
|
// https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
|
|
@@ -93,7 +66,6 @@ export function buildMetadata(
|
|
|
93
66
|
//
|
|
94
67
|
'en-US',
|
|
95
68
|
],
|
|
96
|
-
id_token_signing_alg_values_supported: [...keyset.signAlgorithms],
|
|
97
69
|
display_values_supported: [
|
|
98
70
|
//
|
|
99
71
|
'page',
|
|
@@ -110,10 +82,6 @@ export function buildMetadata(
|
|
|
110
82
|
request_object_encryption_alg_values_supported: [], // None
|
|
111
83
|
request_object_encryption_enc_values_supported: [], // None
|
|
112
84
|
|
|
113
|
-
// No claim makes sense to be translated
|
|
114
|
-
claims_locales_supported: [],
|
|
115
|
-
|
|
116
|
-
claims_parameter_supported: true,
|
|
117
85
|
request_parameter_supported: true,
|
|
118
86
|
request_uri_parameter_supported: true,
|
|
119
87
|
require_request_uri_registration: true,
|
|
@@ -130,7 +98,6 @@ export function buildMetadata(
|
|
|
130
98
|
|
|
131
99
|
introspection_endpoint: new URL('/oauth/introspect', issuer).href,
|
|
132
100
|
|
|
133
|
-
userinfo_endpoint: new URL('/oauth/userinfo', issuer).href,
|
|
134
101
|
// end_session_endpoint: new URL('/oauth/logout', issuer).href,
|
|
135
102
|
|
|
136
103
|
// https://datatracker.ietf.org/doc/html/rfc9126#section-5
|
package/src/oauth-hooks.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { ClientAuth } from './client/client-auth.js'
|
|
|
11
11
|
import { ClientId } from './client/client-id.js'
|
|
12
12
|
import { ClientInfo } from './client/client-info.js'
|
|
13
13
|
import { Client } from './client/client.js'
|
|
14
|
+
import { InvalidAuthorizationDetailsError } from './errors/invalid-authorization-details-error.js'
|
|
14
15
|
import { Awaitable } from './lib/util/type.js'
|
|
15
16
|
|
|
16
17
|
// Make sure all types needed to implement the OAuthHooks are exported
|
|
@@ -20,6 +21,7 @@ export type {
|
|
|
20
21
|
ClientAuth,
|
|
21
22
|
ClientId,
|
|
22
23
|
ClientInfo,
|
|
24
|
+
InvalidAuthorizationDetailsError,
|
|
23
25
|
Jwks,
|
|
24
26
|
OAuthAuthenticationRequestParameters,
|
|
25
27
|
OAuthAuthorizationDetails,
|
|
@@ -42,7 +44,7 @@ export type OAuthHooks = {
|
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* Allows enriching the authorization details with additional information
|
|
45
|
-
*
|
|
47
|
+
* when the tokens are issued.
|
|
46
48
|
*
|
|
47
49
|
* @see {@link https://datatracker.ietf.org/doc/html/rfc9396 | RFC 9396}
|
|
48
50
|
*/
|
|
@@ -51,16 +53,4 @@ export type OAuthHooks = {
|
|
|
51
53
|
parameters: OAuthAuthenticationRequestParameters
|
|
52
54
|
account: Account
|
|
53
55
|
}) => Awaitable<undefined | OAuthAuthorizationDetails>
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Allows altering the token response before it is sent to the client.
|
|
57
|
-
*/
|
|
58
|
-
onTokenResponse?: (
|
|
59
|
-
tokenResponse: OAuthTokenResponse,
|
|
60
|
-
data: {
|
|
61
|
-
client: Client
|
|
62
|
-
parameters: OAuthAuthenticationRequestParameters
|
|
63
|
-
account: Account
|
|
64
|
-
},
|
|
65
|
-
) => Awaitable<void>
|
|
66
56
|
}
|