@atproto/oauth-provider 0.2.0 → 0.2.2
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 +42 -0
- package/dist/account/account-store.d.ts +2 -2
- package/dist/assets/app/bundle-manifest.json +3 -3
- package/dist/assets/app/main.css +1 -1
- package/dist/assets/app/main.js +3 -3
- package/dist/assets/app/main.js.map +1 -1
- package/dist/assets/assets-middleware.d.ts.map +1 -1
- package/dist/assets/assets-middleware.js +4 -2
- package/dist/assets/assets-middleware.js.map +1 -1
- package/dist/client/client-manager.d.ts.map +1 -1
- package/dist/client/client-manager.js +127 -118
- package/dist/client/client-manager.js.map +1 -1
- package/dist/client/client-utils.d.ts +1 -2
- package/dist/client/client-utils.d.ts.map +1 -1
- package/dist/client/client-utils.js +3 -12
- package/dist/client/client-utils.js.map +1 -1
- package/dist/client/client.d.ts +8 -3
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +70 -1
- package/dist/client/client.js.map +1 -1
- package/dist/constants.d.ts +0 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -2
- package/dist/constants.js.map +1 -1
- package/dist/errors/access-denied-error.d.ts +4 -4
- package/dist/errors/access-denied-error.d.ts.map +1 -1
- package/dist/errors/access-denied-error.js +2 -2
- package/dist/errors/access-denied-error.js.map +1 -1
- package/dist/errors/account-selection-required-error.d.ts +2 -2
- package/dist/errors/account-selection-required-error.d.ts.map +1 -1
- package/dist/errors/account-selection-required-error.js.map +1 -1
- package/dist/errors/consent-required-error.d.ts +2 -2
- package/dist/errors/consent-required-error.d.ts.map +1 -1
- package/dist/errors/consent-required-error.js.map +1 -1
- package/dist/errors/invalid-authorization-details-error.d.ts +2 -2
- package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
- package/dist/errors/invalid-authorization-details-error.js.map +1 -1
- package/dist/errors/invalid-client-id-error.d.ts +1 -1
- package/dist/errors/invalid-client-id-error.d.ts.map +1 -1
- package/dist/errors/invalid-client-id-error.js +12 -6
- package/dist/errors/invalid-client-id-error.js.map +1 -1
- package/dist/errors/invalid-client-metadata-error.d.ts +1 -1
- package/dist/errors/invalid-client-metadata-error.d.ts.map +1 -1
- package/dist/errors/invalid-client-metadata-error.js +11 -3
- package/dist/errors/invalid-client-metadata-error.js.map +1 -1
- package/dist/errors/invalid-parameters-error.d.ts +2 -2
- package/dist/errors/invalid-parameters-error.d.ts.map +1 -1
- package/dist/errors/invalid-parameters-error.js.map +1 -1
- package/dist/errors/invalid-scope-error.d.ts +9 -0
- package/dist/errors/invalid-scope-error.d.ts.map +1 -0
- package/dist/errors/invalid-scope-error.js +14 -0
- package/dist/errors/invalid-scope-error.js.map +1 -0
- package/dist/errors/login-required-error.d.ts +2 -2
- package/dist/errors/login-required-error.d.ts.map +1 -1
- package/dist/errors/login-required-error.js.map +1 -1
- package/dist/lib/html/html.d.ts +1 -1
- package/dist/lib/html/html.d.ts.map +1 -1
- package/dist/lib/html/html.js +14 -11
- package/dist/lib/html/html.js.map +1 -1
- package/dist/lib/http/parser.d.ts +9 -2
- package/dist/lib/http/parser.d.ts.map +1 -1
- package/dist/lib/http/parser.js +15 -7
- package/dist/lib/http/parser.js.map +1 -1
- package/dist/lib/http/request.d.ts +0 -23
- package/dist/lib/http/request.d.ts.map +1 -1
- package/dist/lib/http/request.js +1 -11
- package/dist/lib/http/request.js.map +1 -1
- package/dist/lib/http/stream.d.ts +28 -6
- package/dist/lib/http/stream.d.ts.map +1 -1
- package/dist/lib/http/stream.js +21 -32
- package/dist/lib/http/stream.js.map +1 -1
- package/dist/lib/util/authorization-header.d.ts.map +1 -1
- package/dist/lib/util/authorization-header.js +1 -1
- package/dist/lib/util/authorization-header.js.map +1 -1
- package/dist/lib/util/hostname.d.ts +3 -2
- package/dist/lib/util/hostname.d.ts.map +1 -1
- package/dist/lib/util/hostname.js +12 -8
- package/dist/lib/util/hostname.js.map +1 -1
- package/dist/metadata/build-metadata.d.ts.map +1 -1
- package/dist/metadata/build-metadata.js +2 -1
- package/dist/metadata/build-metadata.js.map +1 -1
- package/dist/oauth-errors.d.ts +1 -0
- package/dist/oauth-errors.d.ts.map +1 -1
- package/dist/oauth-errors.js +3 -1
- package/dist/oauth-errors.js.map +1 -1
- package/dist/oauth-hooks.d.ts +3 -3
- package/dist/oauth-hooks.d.ts.map +1 -1
- package/dist/oauth-provider.d.ts +20 -22
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +234 -176
- package/dist/oauth-provider.js.map +1 -1
- package/dist/oauth-verifier.d.ts +2 -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 +2 -2
- package/dist/output/build-authorize-data.d.ts.map +1 -1
- package/dist/output/send-authorize-redirect.d.ts +2 -4
- package/dist/output/send-authorize-redirect.d.ts.map +1 -1
- package/dist/output/send-authorize-redirect.js +5 -2
- package/dist/output/send-authorize-redirect.js.map +1 -1
- package/dist/request/request-data.d.ts +2 -2
- package/dist/request/request-data.d.ts.map +1 -1
- package/dist/request/request-info.d.ts +2 -2
- package/dist/request/request-info.d.ts.map +1 -1
- package/dist/request/request-manager.d.ts +4 -4
- package/dist/request/request-manager.d.ts.map +1 -1
- package/dist/request/request-manager.js +94 -60
- package/dist/request/request-manager.js.map +1 -1
- package/dist/signer/signed-token-payload.d.ts +122 -122
- package/dist/signer/signer.d.ts +41 -40
- package/dist/signer/signer.d.ts.map +1 -1
- package/dist/signer/signer.js +13 -15
- package/dist/signer/signer.js.map +1 -1
- package/dist/token/token-claims.d.ts +121 -121
- package/dist/token/token-data.d.ts +3 -3
- package/dist/token/token-data.d.ts.map +1 -1
- package/dist/token/token-manager.d.ts +4 -5
- package/dist/token/token-manager.d.ts.map +1 -1
- package/dist/token/token-manager.js +96 -72
- package/dist/token/token-manager.js.map +1 -1
- package/dist/token/verify-token-claims.d.ts +3 -3
- package/dist/token/verify-token-claims.d.ts.map +1 -1
- package/dist/token/verify-token-claims.js.map +1 -1
- package/package.json +7 -6
- package/src/assets/app/components/sign-in-form.tsx +31 -2
- package/src/assets/app/components/url-viewer.tsx +3 -3
- package/src/assets/assets-middleware.ts +4 -2
- package/src/client/client-manager.ts +163 -161
- package/src/client/client-utils.ts +7 -12
- package/src/client/client.ts +112 -3
- package/src/constants.ts +0 -2
- package/src/errors/access-denied-error.ts +10 -4
- package/src/errors/account-selection-required-error.ts +2 -2
- package/src/errors/consent-required-error.ts +2 -2
- package/src/errors/invalid-authorization-details-error.ts +2 -2
- package/src/errors/invalid-client-id-error.ts +15 -4
- package/src/errors/invalid-client-metadata-error.ts +15 -3
- package/src/errors/invalid-parameters-error.ts +2 -2
- package/src/errors/invalid-scope-error.ts +15 -0
- package/src/errors/login-required-error.ts +2 -2
- package/src/lib/html/html.ts +14 -12
- package/src/lib/http/parser.ts +21 -8
- package/src/lib/http/request.ts +1 -23
- package/src/lib/http/stream.ts +29 -60
- package/src/lib/util/authorization-header.ts +5 -2
- package/src/lib/util/hostname.ts +9 -5
- package/src/metadata/build-metadata.ts +3 -1
- package/src/oauth-errors.ts +1 -0
- package/src/oauth-hooks.ts +3 -3
- package/src/oauth-provider.ts +368 -269
- package/src/oauth-verifier.ts +2 -2
- package/src/output/build-authorize-data.ts +2 -2
- package/src/output/send-authorize-redirect.ts +7 -6
- package/src/request/request-data.ts +2 -2
- package/src/request/request-info.ts +2 -2
- package/src/request/request-manager.ts +129 -103
- package/src/signer/signer.ts +24 -25
- package/src/token/token-data.ts +3 -3
- package/src/token/token-manager.ts +141 -99
- package/src/token/verify-token-claims.ts +3 -3
- package/dist/request/types.d.ts +0 -328
- package/dist/request/types.d.ts.map +0 -1
- package/dist/request/types.js +0 -27
- package/dist/request/types.js.map +0 -1
- package/dist/token/types.d.ts +0 -250
- package/dist/token/types.d.ts.map +0 -1
- package/dist/token/types.js +0 -36
- package/dist/token/types.js.map +0 -1
- package/src/request/types.ts +0 -48
- package/src/token/types.ts +0 -86
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
bindFetch,
|
|
3
3
|
Fetch,
|
|
4
|
+
FetchError,
|
|
4
5
|
fetchJsonProcessor,
|
|
5
6
|
fetchJsonZodProcessor,
|
|
6
7
|
fetchOkProcessor,
|
|
8
|
+
FetchResponseError,
|
|
7
9
|
} from '@atproto-labs/fetch'
|
|
8
10
|
import { pipe } from '@atproto-labs/pipe'
|
|
9
11
|
import {
|
|
@@ -14,7 +16,6 @@ import {
|
|
|
14
16
|
import { Jwks, jwksSchema, Keyset } from '@atproto/jwk'
|
|
15
17
|
import {
|
|
16
18
|
isLoopbackHost,
|
|
17
|
-
isLoopbackUrl,
|
|
18
19
|
isOAuthClientIdDiscoverable,
|
|
19
20
|
isOAuthClientIdLoopback,
|
|
20
21
|
OAuthAuthorizationServerMetadata,
|
|
@@ -24,12 +25,16 @@ import {
|
|
|
24
25
|
OAuthClientMetadataInput,
|
|
25
26
|
oauthClientMetadataSchema,
|
|
26
27
|
} from '@atproto/oauth-types'
|
|
28
|
+
import { ZodError } from 'zod'
|
|
27
29
|
|
|
28
|
-
import { ALLOW_LOOPBACK_CLIENT_REFRESH_TOKEN } from '../constants.js'
|
|
29
30
|
import { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'
|
|
30
31
|
import { InvalidRedirectUriError } from '../errors/invalid-redirect-uri-error.js'
|
|
31
32
|
import { OAuthError } from '../errors/oauth-error.js'
|
|
32
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
isInternetHost,
|
|
35
|
+
isInternetUrl,
|
|
36
|
+
parseUrlPublicSuffix,
|
|
37
|
+
} from '../lib/util/hostname.js'
|
|
33
38
|
import { Awaitable } from '../lib/util/type.js'
|
|
34
39
|
import { OAuthHooks } from '../oauth-hooks.js'
|
|
35
40
|
import { ClientId } from './client-id.js'
|
|
@@ -106,20 +111,38 @@ export class ClientManager {
|
|
|
106
111
|
})
|
|
107
112
|
|
|
108
113
|
const isFirstParty = partialInfo?.isFirstParty ?? false
|
|
109
|
-
const isTrusted =
|
|
110
|
-
partialInfo?.isTrusted ??
|
|
111
|
-
(isFirstParty ||
|
|
112
|
-
// If the client was loaded from the store, we consider it trusted:
|
|
113
|
-
(!isOAuthClientIdLoopback(clientId) &&
|
|
114
|
-
!isOAuthClientIdDiscoverable(clientId)))
|
|
114
|
+
const isTrusted = partialInfo?.isTrusted ?? isFirstParty
|
|
115
115
|
|
|
116
116
|
return new Client(clientId, metadata, jwks, { isFirstParty, isTrusted })
|
|
117
117
|
} catch (err) {
|
|
118
|
-
if (err instanceof OAuthError)
|
|
118
|
+
if (err instanceof OAuthError) {
|
|
119
|
+
throw err
|
|
120
|
+
}
|
|
121
|
+
if (err instanceof FetchError) {
|
|
122
|
+
const message =
|
|
123
|
+
err instanceof FetchResponseError || err.statusCode !== 500
|
|
124
|
+
? // Only expose 500 message if it was generated on another server
|
|
125
|
+
`Failed to fetch client information: ${err.message}`
|
|
126
|
+
: `Failed to fetch client information due to an internal error`
|
|
127
|
+
throw new InvalidClientMetadataError(message, err)
|
|
128
|
+
}
|
|
129
|
+
if (err instanceof ZodError) {
|
|
130
|
+
const issues = err.issues
|
|
131
|
+
.map(
|
|
132
|
+
({ path, message }) =>
|
|
133
|
+
`Validation${path.length ? ` of "${path.join('.')}"` : ''} failed with error: ${message}`,
|
|
134
|
+
)
|
|
135
|
+
.join(' ')
|
|
136
|
+
throw new InvalidClientMetadataError(issues || err.message, err)
|
|
137
|
+
}
|
|
119
138
|
if (err?.['code'] === 'DEPTH_ZERO_SELF_SIGNED_CERT') {
|
|
120
139
|
throw new InvalidClientMetadataError('Self-signed certificate', err)
|
|
121
140
|
}
|
|
122
|
-
|
|
141
|
+
|
|
142
|
+
throw InvalidClientMetadataError.from(
|
|
143
|
+
err,
|
|
144
|
+
`Unable to load client information for "${clientId}"`,
|
|
145
|
+
)
|
|
123
146
|
}
|
|
124
147
|
}
|
|
125
148
|
|
|
@@ -145,15 +168,11 @@ export class ClientManager {
|
|
|
145
168
|
throw new InvalidClientMetadataError('Loopback clients are not allowed')
|
|
146
169
|
}
|
|
147
170
|
|
|
148
|
-
const
|
|
171
|
+
const metadata = oauthClientMetadataSchema.parse(
|
|
149
172
|
await loopbackMetadata(clientId),
|
|
150
173
|
)
|
|
151
174
|
|
|
152
|
-
|
|
153
|
-
throw InvalidClientMetadataError.from(result.error)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return this.validateClientMetadata(clientId, result.data)
|
|
175
|
+
return this.validateClientMetadata(clientId, metadata)
|
|
157
176
|
}
|
|
158
177
|
|
|
159
178
|
protected async getDiscoverableClientMetadata(
|
|
@@ -212,26 +231,34 @@ export class ClientManager {
|
|
|
212
231
|
const clientUriUrl = metadata.client_uri
|
|
213
232
|
? new URL(metadata.client_uri)
|
|
214
233
|
: null
|
|
215
|
-
const
|
|
234
|
+
const clientUriDomain = clientUriUrl
|
|
235
|
+
? parseUrlPublicSuffix(clientUriUrl)
|
|
236
|
+
: null
|
|
216
237
|
|
|
217
|
-
if (clientUriUrl && !
|
|
218
|
-
throw new InvalidClientMetadataError('client_uri
|
|
238
|
+
if (clientUriUrl && !clientUriDomain) {
|
|
239
|
+
throw new InvalidClientMetadataError('client_uri hostname is invalid')
|
|
219
240
|
}
|
|
220
241
|
|
|
221
|
-
const scopes = metadata.scope?.split(' ')
|
|
242
|
+
const scopes = metadata.scope?.split(' ')
|
|
243
|
+
|
|
244
|
+
if (!scopes) {
|
|
245
|
+
throw new InvalidClientMetadataError('Missing scope property')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!scopes.includes('atproto')) {
|
|
249
|
+
throw new InvalidClientMetadataError('Missing "atproto" scope')
|
|
250
|
+
}
|
|
222
251
|
|
|
223
252
|
const dupScope = scopes?.find(isDuplicate)
|
|
224
253
|
if (dupScope) {
|
|
225
254
|
throw new InvalidClientMetadataError(`Duplicate scope "${dupScope}"`)
|
|
226
255
|
}
|
|
227
256
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
throw new InvalidClientMetadataError(`Unsupported scope "${scope}"`)
|
|
234
|
-
}
|
|
257
|
+
for (const scope of scopes) {
|
|
258
|
+
// Note, once we have dynamic scopes, this check will need to be
|
|
259
|
+
// updated to check against the server's supported scopes.
|
|
260
|
+
if (!this.serverMetadata.scopes_supported?.includes(scope)) {
|
|
261
|
+
throw new InvalidClientMetadataError(`Unsupported scope "${scope}"`)
|
|
235
262
|
}
|
|
236
263
|
}
|
|
237
264
|
|
|
@@ -244,14 +271,24 @@ export class ClientManager {
|
|
|
244
271
|
|
|
245
272
|
for (const grantType of metadata.grant_types) {
|
|
246
273
|
switch (grantType) {
|
|
247
|
-
case 'authorization_code':
|
|
248
|
-
case 'refresh_token':
|
|
249
|
-
continue
|
|
250
274
|
case 'implicit':
|
|
251
|
-
|
|
275
|
+
// Never allowed (unsafe)
|
|
252
276
|
throw new InvalidClientMetadataError(
|
|
253
277
|
`Grant type "${grantType}" is not allowed`,
|
|
254
278
|
)
|
|
279
|
+
|
|
280
|
+
// @TODO: Add support (e.g. for first party client)
|
|
281
|
+
// case 'client_credentials':
|
|
282
|
+
// case 'password':
|
|
283
|
+
case 'authorization_code':
|
|
284
|
+
case 'refresh_token':
|
|
285
|
+
if (!this.serverMetadata.grant_types_supported?.includes(grantType)) {
|
|
286
|
+
throw new InvalidClientMetadataError(
|
|
287
|
+
`Unsupported grant type "${grantType}"`,
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
break
|
|
291
|
+
|
|
255
292
|
default:
|
|
256
293
|
throw new InvalidClientMetadataError(
|
|
257
294
|
`Grant type "${grantType}" is not supported`,
|
|
@@ -336,41 +373,14 @@ export class ClientManager {
|
|
|
336
373
|
)
|
|
337
374
|
}
|
|
338
375
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
)
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// ATPROTO spec requires the use of PKCE
|
|
347
|
-
if (responseType !== 'code') {
|
|
348
|
-
throw new InvalidClientMetadataError(
|
|
349
|
-
`Unsupported response type "${responseType}"`,
|
|
350
|
-
)
|
|
351
|
-
}
|
|
352
|
-
|
|
376
|
+
// ATPROTO spec requires the use of PKCE, does not support OIDC
|
|
377
|
+
if (!metadata.response_types.includes('code')) {
|
|
378
|
+
throw new InvalidClientMetadataError('response_types must include "code"')
|
|
379
|
+
} else if (!metadata.grant_types.includes('authorization_code')) {
|
|
353
380
|
// Consistency check
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
) {
|
|
358
|
-
throw new InvalidClientMetadataError(
|
|
359
|
-
`Response type "${responseType}" requires the "authorization_code" grant type`,
|
|
360
|
-
)
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (metadata.application_type === 'native') {
|
|
365
|
-
// https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
|
|
366
|
-
//
|
|
367
|
-
// > Except when using a mechanism like Dynamic Client Registration
|
|
368
|
-
// > [RFC7591] to provision per-instance secrets, native apps are
|
|
369
|
-
// > classified as public clients, as defined by Section 2.1 of OAuth 2.0
|
|
370
|
-
// > [RFC6749]; they MUST be registered with the authorization server as
|
|
371
|
-
// > such. Authorization servers MUST record the client type in the
|
|
372
|
-
// > client registration details in order to identify and process requests
|
|
373
|
-
// > accordingly.
|
|
381
|
+
throw new InvalidClientMetadataError(
|
|
382
|
+
`The "code" response type requires that "grant_types" contains "authorization_code"`,
|
|
383
|
+
)
|
|
374
384
|
}
|
|
375
385
|
|
|
376
386
|
if (metadata.authorization_details_types?.length) {
|
|
@@ -433,47 +443,6 @@ export class ClientManager {
|
|
|
433
443
|
}
|
|
434
444
|
}
|
|
435
445
|
|
|
436
|
-
if (metadata.application_type === 'native') {
|
|
437
|
-
// https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
|
|
438
|
-
//
|
|
439
|
-
// > Native Clients [as defined by "application_type"] MUST only
|
|
440
|
-
// > register redirect_uris using custom URI schemes or loopback URLs
|
|
441
|
-
// > using the http scheme; loopback URLs use localhost or the IP
|
|
442
|
-
// > loopback literals 127.0.0.1 or [::1] as the hostname.
|
|
443
|
-
|
|
444
|
-
for (const redirectUri of metadata.redirect_uris) {
|
|
445
|
-
const url = parseRedirectUri(redirectUri)
|
|
446
|
-
if (url.protocol !== 'http:') {
|
|
447
|
-
throw new InvalidRedirectUriError(
|
|
448
|
-
`Native clients must use HTTP redirect URIs (got ${url})`,
|
|
449
|
-
)
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (!isLoopbackHost(url.hostname) && !isPrivateUseUriScheme(url)) {
|
|
453
|
-
throw new InvalidRedirectUriError(
|
|
454
|
-
'Loopback redirect URIs are only allowed for native apps',
|
|
455
|
-
)
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (metadata.application_type === 'native') {
|
|
461
|
-
// https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
|
|
462
|
-
//
|
|
463
|
-
// > Authorization Servers MAY reject Redirection URI values using
|
|
464
|
-
// > the http scheme, other than the loopback case for Native
|
|
465
|
-
// > Clients.
|
|
466
|
-
|
|
467
|
-
for (const redirectUri of metadata.redirect_uris) {
|
|
468
|
-
const url = parseRedirectUri(redirectUri)
|
|
469
|
-
if (url.protocol === 'http:' && !isLoopbackUrl(url)) {
|
|
470
|
-
throw new InvalidRedirectUriError(
|
|
471
|
-
`Native clients must not use HTTP redirect URIs (got ${url})`,
|
|
472
|
-
)
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
446
|
for (const redirectUri of metadata.redirect_uris) {
|
|
478
447
|
const url = parseRedirectUri(redirectUri)
|
|
479
448
|
|
|
@@ -502,16 +471,10 @@ export class ClientManager {
|
|
|
502
471
|
`Loopback redirect URI ${url} is not allowed (use explicit IPs instead)`,
|
|
503
472
|
)
|
|
504
473
|
}
|
|
505
|
-
|
|
474
|
+
|
|
506
475
|
case url.hostname === '127.0.0.1':
|
|
507
476
|
case url.hostname === '[::1]': {
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
// > Loopback redirect URIs use the "http" scheme and are constructed
|
|
511
|
-
// > with the loopback IP literal and whatever port the client is
|
|
512
|
-
// > listening on. That is, "http://127.0.0.1:{port}/{path}" for IPv4,
|
|
513
|
-
// > and "http://[::1]:{port}/{path}" for IPv6.
|
|
514
|
-
|
|
477
|
+
// Only allowed for native apps
|
|
515
478
|
if (metadata.application_type !== 'native') {
|
|
516
479
|
throw new InvalidRedirectUriError(
|
|
517
480
|
`Loopback redirect URIs are only allowed for native apps`,
|
|
@@ -534,6 +497,12 @@ export class ClientManager {
|
|
|
534
497
|
}
|
|
535
498
|
|
|
536
499
|
if (url.protocol !== 'http:') {
|
|
500
|
+
// https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
|
|
501
|
+
//
|
|
502
|
+
// > Loopback redirect URIs use the "http" scheme and are constructed
|
|
503
|
+
// > with the loopback IP literal and whatever port the client is
|
|
504
|
+
// > listening on. That is, "http://127.0.0.1:{port}/{path}" for IPv4,
|
|
505
|
+
// > and "http://[::1]:{port}/{path}" for IPv6.
|
|
537
506
|
throw new InvalidRedirectUriError(
|
|
538
507
|
`Loopback redirect URI ${url} must use HTTP`,
|
|
539
508
|
)
|
|
@@ -551,17 +520,23 @@ export class ClientManager {
|
|
|
551
520
|
// > target Request Object is signed in a way that is verifiable by
|
|
552
521
|
// > the OP.
|
|
553
522
|
//
|
|
554
|
-
//
|
|
523
|
+
// OIDC/Request Object are not supported. ATproto spec should not
|
|
524
|
+
// allow HTTP redirect URIs either.
|
|
525
|
+
|
|
526
|
+
// https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
|
|
527
|
+
//
|
|
528
|
+
// > Authorization Servers MAY reject Redirection URI values using
|
|
529
|
+
// > the http scheme, other than the loopback case for Native
|
|
530
|
+
// > Clients.
|
|
555
531
|
throw new InvalidRedirectUriError(
|
|
556
|
-
|
|
532
|
+
'Only loopback redirect URIs are allowed to use the "http" scheme',
|
|
557
533
|
)
|
|
558
534
|
}
|
|
559
535
|
|
|
560
536
|
case url.protocol === 'https:': {
|
|
561
|
-
|
|
562
|
-
if (!redirectUriDomain) {
|
|
537
|
+
if (!isInternetUrl(url)) {
|
|
563
538
|
throw new InvalidRedirectUriError(
|
|
564
|
-
`Redirect URI ${url} must
|
|
539
|
+
`Redirect URI "${url}"'s domain name must belong to the Public Suffix List (PSL)`,
|
|
565
540
|
)
|
|
566
541
|
}
|
|
567
542
|
|
|
@@ -573,19 +548,29 @@ export class ClientManager {
|
|
|
573
548
|
// > where two apps claim the same private-use URI scheme (where one
|
|
574
549
|
// > app is acting maliciously).
|
|
575
550
|
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
551
|
+
// We can't enforce this here (in generic client validation) because
|
|
552
|
+
// we don't have a concept of generic proven ownership.
|
|
553
|
+
//
|
|
554
|
+
// Discoverable clients, however, will have this check covered in the
|
|
555
|
+
// `validateDiscoverableClientMetadata`, by using the client_id's
|
|
556
|
+
// domain as "proven ownership".
|
|
557
|
+
|
|
558
|
+
// The following restriction from OIDC is *not* enforced for clients
|
|
559
|
+
// as it prevents "App Links" / "Apple Universal Links" from being
|
|
560
|
+
// used as redirect URIs.
|
|
561
|
+
//
|
|
562
|
+
// https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
|
|
563
|
+
//
|
|
564
|
+
// > Native Clients [as defined by "application_type"] MUST only
|
|
565
|
+
// > register redirect_uris using custom URI schemes or loopback URLs
|
|
566
|
+
// > using the http scheme; loopback URLs use localhost or the IP
|
|
567
|
+
// > loopback literals 127.0.0.1 or [::1] as the hostname.
|
|
568
|
+
//
|
|
569
|
+
// if (metadata.application_type === 'native') {
|
|
570
|
+
// throw new InvalidRedirectUriError(
|
|
571
|
+
// `Native clients must use custom URI schemes or loopback URLs`,
|
|
572
|
+
// )
|
|
573
|
+
// }
|
|
589
574
|
|
|
590
575
|
break
|
|
591
576
|
}
|
|
@@ -604,16 +589,6 @@ export class ClientManager {
|
|
|
604
589
|
)
|
|
605
590
|
}
|
|
606
591
|
|
|
607
|
-
const redirectUriDomain = parseDomain(
|
|
608
|
-
reverseDomain(url.protocol.slice(0, -1)),
|
|
609
|
-
)
|
|
610
|
-
|
|
611
|
-
if (!redirectUriDomain) {
|
|
612
|
-
throw new InvalidRedirectUriError(
|
|
613
|
-
`Private-use URI Scheme redirect URI must be based on a valid domain name`,
|
|
614
|
-
)
|
|
615
|
-
}
|
|
616
|
-
|
|
617
592
|
// https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
|
|
618
593
|
//
|
|
619
594
|
// > In addition to the collision-resistant properties, requiring a
|
|
@@ -621,16 +596,17 @@ export class ClientManager {
|
|
|
621
596
|
// > the app can help to prove ownership in the event of a dispute
|
|
622
597
|
// > where two apps claim the same private-use URI scheme (where one
|
|
623
598
|
// > app is acting maliciously).
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
599
|
+
//
|
|
600
|
+
// We can't check for ownership here (as there is no concept of
|
|
601
|
+
// proven ownership in the generic client validation), but we can
|
|
602
|
+
// check that the domain is a valid domain name.
|
|
603
|
+
|
|
604
|
+
const urlDomain = reverseDomain(url.protocol.slice(0, -1))
|
|
605
|
+
|
|
606
|
+
if (!isInternetHost(urlDomain)) {
|
|
607
|
+
throw new InvalidRedirectUriError(
|
|
608
|
+
`Private-use URI Scheme redirect URI must be based on a valid domain name`,
|
|
627
609
|
)
|
|
628
|
-
} else {
|
|
629
|
-
if (redirectUriDomain.domain !== clientUriParsed.domain) {
|
|
630
|
-
throw new InvalidRedirectUriError(
|
|
631
|
-
`Private-Use URI Scheme redirect URI ${url} must be under the same domain as client_uri ${metadata.client_uri}`,
|
|
632
|
-
)
|
|
633
|
-
}
|
|
634
610
|
}
|
|
635
611
|
|
|
636
612
|
// https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
|
|
@@ -689,15 +665,6 @@ export class ClientManager {
|
|
|
689
665
|
)
|
|
690
666
|
}
|
|
691
667
|
|
|
692
|
-
if (
|
|
693
|
-
!ALLOW_LOOPBACK_CLIENT_REFRESH_TOKEN &&
|
|
694
|
-
metadata.grant_types.includes('refresh_token')
|
|
695
|
-
) {
|
|
696
|
-
throw new InvalidClientMetadataError(
|
|
697
|
-
'Loopback clients are not allowed to use the "refresh_token" grant type',
|
|
698
|
-
)
|
|
699
|
-
}
|
|
700
|
-
|
|
701
668
|
const method = metadata[`token_endpoint_auth_method`]
|
|
702
669
|
if (method !== 'none') {
|
|
703
670
|
throw new InvalidClientMetadataError(
|
|
@@ -768,18 +735,34 @@ export class ClientManager {
|
|
|
768
735
|
|
|
769
736
|
const method = metadata[`token_endpoint_auth_method`]
|
|
770
737
|
switch (method) {
|
|
738
|
+
case 'none':
|
|
739
|
+
case 'private_key_jwt':
|
|
740
|
+
case undefined:
|
|
741
|
+
break
|
|
771
742
|
case 'client_secret_post':
|
|
772
743
|
case 'client_secret_basic':
|
|
773
744
|
case 'client_secret_jwt':
|
|
774
745
|
throw new InvalidClientMetadataError(
|
|
775
746
|
`Client authentication method "${method}" is not allowed for discoverable clients`,
|
|
776
747
|
)
|
|
748
|
+
default:
|
|
749
|
+
throw new InvalidClientMetadataError(
|
|
750
|
+
`Unsupported client authentication method "${method}"`,
|
|
751
|
+
)
|
|
777
752
|
}
|
|
778
753
|
|
|
779
754
|
for (const redirectUri of metadata.redirect_uris) {
|
|
780
755
|
const url = parseRedirectUri(redirectUri)
|
|
781
756
|
|
|
782
757
|
if (isPrivateUseUriScheme(url)) {
|
|
758
|
+
// https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
|
|
759
|
+
//
|
|
760
|
+
// > In addition to the collision-resistant properties, requiring a
|
|
761
|
+
// > URI scheme based on a domain name that is under the control of
|
|
762
|
+
// > the app can help to prove ownership in the event of a dispute
|
|
763
|
+
// > where two apps claim the same private-use URI scheme (where one
|
|
764
|
+
// > app is acting maliciously).
|
|
765
|
+
|
|
783
766
|
// https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html
|
|
784
767
|
//
|
|
785
768
|
// Fully qualified domain name (FQDN) of the client_id, in reverse
|
|
@@ -792,6 +775,25 @@ export class ClientManager {
|
|
|
792
775
|
)
|
|
793
776
|
}
|
|
794
777
|
}
|
|
778
|
+
|
|
779
|
+
if (url.protocol === 'https:') {
|
|
780
|
+
// https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
|
|
781
|
+
//
|
|
782
|
+
// > In addition to the collision-resistant properties, requiring a
|
|
783
|
+
// > URI scheme based on a domain name that is under the control of
|
|
784
|
+
// > the app can help to prove ownership in the event of a dispute
|
|
785
|
+
// > where two apps claim the same private-use URI scheme (where one
|
|
786
|
+
// > app is acting maliciously).
|
|
787
|
+
//
|
|
788
|
+
// Although this only applies to "native" clients (extract being from
|
|
789
|
+
// rfc8252), we apply this rule to "web" clients as well.
|
|
790
|
+
|
|
791
|
+
if (url.hostname !== clientIdUrl.hostname) {
|
|
792
|
+
throw new InvalidRedirectUriError(
|
|
793
|
+
`Redirect URI ${url} must be under the same domain as client_id ${metadata.client_uri}`,
|
|
794
|
+
)
|
|
795
|
+
}
|
|
796
|
+
}
|
|
795
797
|
}
|
|
796
798
|
|
|
797
799
|
return metadata
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
OAuthClientIdDiscoverable,
|
|
3
|
-
OAuthClientIdLoopback,
|
|
4
|
-
parseOAuthLoopbackClientId,
|
|
5
3
|
parseOAuthDiscoverableClientId,
|
|
6
4
|
} from '@atproto/oauth-types'
|
|
7
5
|
|
|
@@ -25,19 +23,16 @@ export function parseDiscoverableClientId(
|
|
|
25
23
|
|
|
26
24
|
// Extra validation, prevent usage of invalid internet domain names.
|
|
27
25
|
if (!isInternetHost(url.hostname)) {
|
|
28
|
-
throw new InvalidClientIdError(
|
|
26
|
+
throw new InvalidClientIdError(
|
|
27
|
+
"The client_id's TLD must belong to the Public Suffix List (PSL)",
|
|
28
|
+
)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
return url
|
|
32
32
|
} catch (err) {
|
|
33
|
-
throw InvalidClientIdError.from(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
export function parseLoopbackClientId(clientId: OAuthClientIdLoopback): URL {
|
|
38
|
-
try {
|
|
39
|
-
return parseOAuthLoopbackClientId(clientId)
|
|
40
|
-
} catch (err) {
|
|
41
|
-
throw InvalidClientIdError.from(err)
|
|
33
|
+
throw InvalidClientIdError.from(
|
|
34
|
+
err,
|
|
35
|
+
'Invalid discoverable client identifier',
|
|
36
|
+
)
|
|
42
37
|
}
|
|
43
38
|
}
|
package/src/client/client.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Jwks } from '@atproto/jwk'
|
|
2
2
|
import {
|
|
3
3
|
CLIENT_ASSERTION_TYPE_JWT_BEARER,
|
|
4
|
-
|
|
4
|
+
OAuthAuthorizationRequestParameters,
|
|
5
|
+
OAuthClientCredentials,
|
|
5
6
|
OAuthClientMetadata,
|
|
6
7
|
} from '@atproto/oauth-types'
|
|
7
8
|
import {
|
|
@@ -20,9 +21,13 @@ import {
|
|
|
20
21
|
} from 'jose'
|
|
21
22
|
|
|
22
23
|
import { CLIENT_ASSERTION_MAX_AGE, JAR_MAX_AGE } from '../constants.js'
|
|
24
|
+
import { InvalidAuthorizationDetailsError } from '../errors/invalid-authorization-details-error.js'
|
|
23
25
|
import { InvalidClientError } from '../errors/invalid-client-error.js'
|
|
24
26
|
import { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'
|
|
27
|
+
import { InvalidParametersError } from '../errors/invalid-parameters-error.js'
|
|
25
28
|
import { InvalidRequestError } from '../errors/invalid-request-error.js'
|
|
29
|
+
import { InvalidScopeError } from '../errors/invalid-scope-error.js'
|
|
30
|
+
import { compareRedirectUri } from '../lib/util/redirect-uri.js'
|
|
26
31
|
import { ClientAuth, authJwkThumbprint } from './client-auth.js'
|
|
27
32
|
import { ClientId } from './client-id.js'
|
|
28
33
|
import { ClientInfo } from './client-info.js'
|
|
@@ -102,11 +107,11 @@ export class Client {
|
|
|
102
107
|
|
|
103
108
|
/**
|
|
104
109
|
* @see {@link https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1}
|
|
105
|
-
* @see {@link https://datatracker.ietf.org/doc/html/
|
|
110
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc7523#section-3}
|
|
106
111
|
* @see {@link https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-endpoint-auth-method}
|
|
107
112
|
*/
|
|
108
113
|
public async verifyCredentials(
|
|
109
|
-
input:
|
|
114
|
+
input: OAuthClientCredentials,
|
|
110
115
|
checks: {
|
|
111
116
|
audience: string
|
|
112
117
|
},
|
|
@@ -218,4 +223,108 @@ export class Client {
|
|
|
218
223
|
// @ts-expect-error
|
|
219
224
|
throw new Error(`Invalid method "${clientAuth.method}"`)
|
|
220
225
|
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Validates the request parameters against the client metadata.
|
|
229
|
+
*/
|
|
230
|
+
public validateRequest(
|
|
231
|
+
parameters: Readonly<OAuthAuthorizationRequestParameters>,
|
|
232
|
+
): Readonly<OAuthAuthorizationRequestParameters> {
|
|
233
|
+
if (parameters.client_id !== this.id) {
|
|
234
|
+
throw new InvalidParametersError(
|
|
235
|
+
parameters,
|
|
236
|
+
'The "client_id" parameter field does not match the value used to authenticate the client',
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (parameters.scope !== undefined) {
|
|
241
|
+
// Any scope requested by the client must be registered in the client
|
|
242
|
+
// metadata.
|
|
243
|
+
const declaredScopes = this.metadata.scope?.split(' ')
|
|
244
|
+
|
|
245
|
+
if (!declaredScopes) {
|
|
246
|
+
throw new InvalidScopeError(
|
|
247
|
+
parameters,
|
|
248
|
+
'Client has no declared scopes in its metadata',
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const scope of parameters.scope.split(' ')) {
|
|
253
|
+
if (!declaredScopes.includes(scope)) {
|
|
254
|
+
throw new InvalidScopeError(
|
|
255
|
+
parameters,
|
|
256
|
+
`Scope "${scope}" is not declared in the client metadata`,
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!this.metadata.response_types.includes(parameters.response_type)) {
|
|
263
|
+
throw new InvalidParametersError(
|
|
264
|
+
parameters,
|
|
265
|
+
`Invalid response_type "${parameters.response_type}" requested by the client`,
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (parameters.response_type.includes('code')) {
|
|
270
|
+
if (!this.metadata.grant_types.includes('authorization_code')) {
|
|
271
|
+
throw new InvalidParametersError(
|
|
272
|
+
parameters,
|
|
273
|
+
`This client is not allowed to use the "authorization_code" grant type`,
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const { redirect_uri } = parameters
|
|
279
|
+
if (redirect_uri) {
|
|
280
|
+
if (
|
|
281
|
+
!this.metadata.redirect_uris.some((uri) =>
|
|
282
|
+
compareRedirectUri(uri, redirect_uri),
|
|
283
|
+
)
|
|
284
|
+
) {
|
|
285
|
+
throw new InvalidParametersError(
|
|
286
|
+
parameters,
|
|
287
|
+
`Invalid redirect_uri ${redirect_uri}`,
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
const { defaultRedirectUri } = this
|
|
292
|
+
if (defaultRedirectUri) {
|
|
293
|
+
parameters = { ...parameters, redirect_uri: defaultRedirectUri }
|
|
294
|
+
} else {
|
|
295
|
+
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#authorization-request
|
|
296
|
+
//
|
|
297
|
+
// > "redirect_uri": OPTIONAL if only one redirect URI is registered for
|
|
298
|
+
// > this client. REQUIRED if multiple redirect URIs are registered for this
|
|
299
|
+
// > client.
|
|
300
|
+
throw new InvalidParametersError(parameters, 'redirect_uri is required')
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (parameters.authorization_details) {
|
|
305
|
+
const { authorization_details_types } = this.metadata
|
|
306
|
+
if (!authorization_details_types) {
|
|
307
|
+
throw new InvalidAuthorizationDetailsError(
|
|
308
|
+
parameters,
|
|
309
|
+
'Client Metadata does not declare any "authorization_details"',
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const detail of parameters.authorization_details) {
|
|
314
|
+
if (!authorization_details_types?.includes(detail.type)) {
|
|
315
|
+
throw new InvalidAuthorizationDetailsError(
|
|
316
|
+
parameters,
|
|
317
|
+
`Client Metadata does not declare any "authorization_details" of type "${detail.type}"`,
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return parameters
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
get defaultRedirectUri(): string | undefined {
|
|
327
|
+
const { redirect_uris } = this.metadata
|
|
328
|
+
return redirect_uris.length === 1 ? redirect_uris[0] : undefined
|
|
329
|
+
}
|
|
221
330
|
}
|