@atproto/oauth-provider 0.19.7 → 0.19.8
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 +26 -0
- package/dist/errors/error-parser.js +5 -5
- package/dist/errors/error-parser.js.map +1 -1
- package/package.json +22 -18
- package/src/access-token/access-token-mode.ts +0 -4
- package/src/account/account-manager.ts +0 -591
- package/src/account/account-store.ts +0 -305
- package/src/account/sign-in-data.ts +0 -15
- package/src/account/sign-up-input.ts +0 -20
- package/src/client/client-auth.ts +0 -64
- package/src/client/client-data.ts +0 -9
- package/src/client/client-id.ts +0 -4
- package/src/client/client-info.ts +0 -13
- package/src/client/client-manager.ts +0 -772
- package/src/client/client-store.ts +0 -35
- package/src/client/client-utils.ts +0 -37
- package/src/client/client.ts +0 -394
- package/src/constants.ts +0 -80
- package/src/customization/branding.ts +0 -12
- package/src/customization/build-customization-css.ts +0 -55
- package/src/customization/build-customization-data.ts +0 -24
- package/src/customization/colors.ts +0 -48
- package/src/customization/customization.ts +0 -29
- package/src/customization/links.ts +0 -10
- package/src/device/device-data.ts +0 -11
- package/src/device/device-id.ts +0 -27
- package/src/device/device-manager.ts +0 -292
- package/src/device/device-store.ts +0 -31
- package/src/device/session-id.ts +0 -21
- package/src/dpop/dpop-manager.ts +0 -214
- package/src/dpop/dpop-nonce.ts +0 -121
- package/src/dpop/dpop-proof.ts +0 -6
- package/src/errors/access-denied-error.ts +0 -12
- package/src/errors/account-selection-required-error.ts +0 -12
- package/src/errors/authorization-error.ts +0 -45
- package/src/errors/consent-required-error.ts +0 -12
- package/src/errors/error-parser.ts +0 -147
- package/src/errors/handle-unavailable-error.ts +0 -23
- package/src/errors/invalid-authorization-details-error.ts +0 -27
- package/src/errors/invalid-client-error.ts +0 -20
- package/src/errors/invalid-client-id-error.ts +0 -31
- package/src/errors/invalid-client-metadata-error.ts +0 -68
- package/src/errors/invalid-credentials-error.ts +0 -29
- package/src/errors/invalid-dpop-key-binding-error.ts +0 -21
- package/src/errors/invalid-dpop-proof-error.ts +0 -13
- package/src/errors/invalid-grant-error.ts +0 -21
- package/src/errors/invalid-invite-code-error.ts +0 -10
- package/src/errors/invalid-redirect-uri-error.ts +0 -17
- package/src/errors/invalid-request-error.ts +0 -32
- package/src/errors/invalid-scope-error.ts +0 -15
- package/src/errors/invalid-token-error.ts +0 -60
- package/src/errors/login-required-error.ts +0 -12
- package/src/errors/oauth-error.ts +0 -28
- package/src/errors/second-authentication-factor-required-error.ts +0 -25
- package/src/errors/unauthorized-client-error.ts +0 -20
- package/src/errors/use-dpop-nonce-error.ts +0 -32
- package/src/errors/www-authenticate-error.ts +0 -64
- package/src/index.ts +0 -16
- package/src/lexicon/lexicon-data.ts +0 -11
- package/src/lexicon/lexicon-getter.ts +0 -55
- package/src/lexicon/lexicon-manager.ts +0 -116
- package/src/lexicon/lexicon-store.ts +0 -35
- package/src/lib/csp/index.ts +0 -101
- package/src/lib/hcaptcha.ts +0 -228
- package/src/lib/html/README.md +0 -9
- package/src/lib/html/build-document.ts +0 -151
- package/src/lib/html/escapers.ts +0 -66
- package/src/lib/html/html.ts +0 -56
- package/src/lib/html/hydration-data.ts +0 -19
- package/src/lib/html/index.ts +0 -5
- package/src/lib/html/tags.ts +0 -67
- package/src/lib/html/util.ts +0 -17
- package/src/lib/http/README.md +0 -11
- package/src/lib/http/accept.ts +0 -90
- package/src/lib/http/context.ts +0 -42
- package/src/lib/http/headers.ts +0 -15
- package/src/lib/http/index.ts +0 -10
- package/src/lib/http/method.ts +0 -18
- package/src/lib/http/middleware.ts +0 -169
- package/src/lib/http/parser.ts +0 -101
- package/src/lib/http/path.ts +0 -82
- package/src/lib/http/request.ts +0 -256
- package/src/lib/http/response.ts +0 -122
- package/src/lib/http/route.ts +0 -60
- package/src/lib/http/router.ts +0 -117
- package/src/lib/http/security-headers.ts +0 -100
- package/src/lib/http/stream.ts +0 -57
- package/src/lib/http/types.ts +0 -16
- package/src/lib/http/url.ts +0 -23
- package/src/lib/nsid.ts +0 -10
- package/src/lib/redis.ts +0 -25
- package/src/lib/util/authorization-header.ts +0 -28
- package/src/lib/util/cast.ts +0 -18
- package/src/lib/util/color.ts +0 -168
- package/src/lib/util/crypto.ts +0 -32
- package/src/lib/util/date.ts +0 -7
- package/src/lib/util/error.ts +0 -7
- package/src/lib/util/function.ts +0 -39
- package/src/lib/util/locale.ts +0 -12
- package/src/lib/util/object.ts +0 -23
- package/src/lib/util/redirect-uri.ts +0 -46
- package/src/lib/util/time.ts +0 -49
- package/src/lib/util/type.ts +0 -182
- package/src/lib/util/ui8.ts +0 -14
- package/src/lib/util/well-known.ts +0 -8
- package/src/lib/util/zod-error.ts +0 -26
- package/src/lib/write-form-redirect.ts +0 -58
- package/src/lib/write-html.ts +0 -70
- package/src/metadata/build-metadata.ts +0 -141
- package/src/oauth-client.ts +0 -3
- package/src/oauth-dpop.ts +0 -2
- package/src/oauth-errors.ts +0 -26
- package/src/oauth-hooks.ts +0 -519
- package/src/oauth-middleware.ts +0 -53
- package/src/oauth-provider.ts +0 -1110
- package/src/oauth-store.ts +0 -12
- package/src/oauth-verifier.ts +0 -260
- package/src/replay/replay-manager.ts +0 -51
- package/src/replay/replay-store-memory.ts +0 -36
- package/src/replay/replay-store-redis.ts +0 -30
- package/src/replay/replay-store.ts +0 -44
- package/src/request/code.ts +0 -23
- package/src/request/request-data.ts +0 -36
- package/src/request/request-id.ts +0 -22
- package/src/request/request-manager.ts +0 -521
- package/src/request/request-store.ts +0 -60
- package/src/request/request-uri.ts +0 -42
- package/src/result/authorization-redirect-parameters.ts +0 -24
- package/src/result/authorization-result-authorize-page.ts +0 -16
- package/src/result/authorization-result-redirect.ts +0 -8
- package/src/router/assets/assets-manifest.ts +0 -117
- package/src/router/assets/assets.ts +0 -124
- package/src/router/assets/csrf.ts +0 -79
- package/src/router/assets/send-account-page.ts +0 -23
- package/src/router/assets/send-authorization-page.ts +0 -42
- package/src/router/assets/send-cookie-error-page.ts +0 -21
- package/src/router/assets/send-error-page.ts +0 -25
- package/src/router/assets/send-redirect.ts +0 -140
- package/src/router/create-account-page-middleware.ts +0 -88
- package/src/router/create-api-middleware.ts +0 -1051
- package/src/router/create-authorization-page-middleware.ts +0 -281
- package/src/router/create-oauth-middleware.ts +0 -257
- package/src/router/error-handler.ts +0 -6
- package/src/router/middleware-options.ts +0 -9
- package/src/signer/access-token-payload.ts +0 -25
- package/src/signer/api-token-payload.ts +0 -18
- package/src/signer/signer.ts +0 -121
- package/src/token/refresh-token.ts +0 -30
- package/src/token/token-claims.ts +0 -22
- package/src/token/token-data.ts +0 -40
- package/src/token/token-id.ts +0 -25
- package/src/token/token-manager.ts +0 -427
- package/src/token/token-store.ts +0 -88
- package/src/types/authorization-response-error.ts +0 -27
- package/src/types/color-hue.ts +0 -3
- package/src/types/email-otp.ts +0 -3
- package/src/types/email.ts +0 -26
- package/src/types/handle.ts +0 -23
- package/src/types/invite-code.ts +0 -4
- package/src/types/par-response-error.ts +0 -25
- package/src/types/password.ts +0 -4
- package/src/types/rgb-color.ts +0 -21
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -4
|
@@ -1,521 +0,0 @@
|
|
|
1
|
-
import { isAtprotoDid } from '@atproto/did'
|
|
2
|
-
import { LexResolverError } from '@atproto/lex-resolver'
|
|
3
|
-
import type { Account } from '@atproto/oauth-provider-api'
|
|
4
|
-
import { isAtprotoOauthScope } from '@atproto/oauth-scopes'
|
|
5
|
-
import {
|
|
6
|
-
OAuthAuthorizationRequestParameters,
|
|
7
|
-
OAuthAuthorizationServerMetadata,
|
|
8
|
-
} from '@atproto/oauth-types'
|
|
9
|
-
import { isValidHandle } from '@atproto/syntax'
|
|
10
|
-
import { ClientAuth } from '../client/client-auth.js'
|
|
11
|
-
import { ClientId } from '../client/client-id.js'
|
|
12
|
-
import { Client } from '../client/client.js'
|
|
13
|
-
import {
|
|
14
|
-
AUTHORIZATION_INACTIVITY_TIMEOUT,
|
|
15
|
-
NODE_ENV,
|
|
16
|
-
PAR_EXPIRES_IN,
|
|
17
|
-
TOKEN_MAX_AGE,
|
|
18
|
-
} from '../constants.js'
|
|
19
|
-
import { DeviceId } from '../device/device-id.js'
|
|
20
|
-
import { AccessDeniedError } from '../errors/access-denied-error.js'
|
|
21
|
-
import { AuthorizationError } from '../errors/authorization-error.js'
|
|
22
|
-
import { ConsentRequiredError } from '../errors/consent-required-error.js'
|
|
23
|
-
import { InvalidAuthorizationDetailsError } from '../errors/invalid-authorization-details-error.js'
|
|
24
|
-
import { InvalidGrantError } from '../errors/invalid-grant-error.js'
|
|
25
|
-
import { InvalidRequestError } from '../errors/invalid-request-error.js'
|
|
26
|
-
import { InvalidScopeError } from '../errors/invalid-scope-error.js'
|
|
27
|
-
import { LexiconManager } from '../lexicon/lexicon-manager.js'
|
|
28
|
-
import { RequestMetadata } from '../lib/http/request.js'
|
|
29
|
-
import { OAuthHooks } from '../oauth-hooks.js'
|
|
30
|
-
import { Signer } from '../signer/signer.js'
|
|
31
|
-
import { Code, generateCode } from './code.js'
|
|
32
|
-
import {
|
|
33
|
-
RequestDataAuthorized,
|
|
34
|
-
isRequestDataAuthorized,
|
|
35
|
-
} from './request-data.js'
|
|
36
|
-
import { generateRequestId } from './request-id.js'
|
|
37
|
-
import { RequestStore, UpdateRequestData } from './request-store.js'
|
|
38
|
-
import {
|
|
39
|
-
RequestUri,
|
|
40
|
-
decodeRequestUri,
|
|
41
|
-
encodeRequestUri,
|
|
42
|
-
} from './request-uri.js'
|
|
43
|
-
|
|
44
|
-
export class RequestManager {
|
|
45
|
-
constructor(
|
|
46
|
-
protected readonly store: RequestStore,
|
|
47
|
-
protected readonly lexiconManager: LexiconManager,
|
|
48
|
-
protected readonly signer: Signer,
|
|
49
|
-
protected readonly metadata: OAuthAuthorizationServerMetadata,
|
|
50
|
-
protected readonly hooks: OAuthHooks,
|
|
51
|
-
protected readonly tokenMaxAge = TOKEN_MAX_AGE,
|
|
52
|
-
) {}
|
|
53
|
-
|
|
54
|
-
protected createTokenExpiry() {
|
|
55
|
-
return new Date(Date.now() + this.tokenMaxAge)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async createAuthorizationRequest(
|
|
59
|
-
client: Client,
|
|
60
|
-
clientAuth: null | ClientAuth,
|
|
61
|
-
input: Readonly<OAuthAuthorizationRequestParameters>,
|
|
62
|
-
deviceId: null | DeviceId,
|
|
63
|
-
) {
|
|
64
|
-
const parameters = await this.validate(client, clientAuth, input)
|
|
65
|
-
|
|
66
|
-
await this.hooks.onAuthorizationRequest?.call(null, {
|
|
67
|
-
client,
|
|
68
|
-
clientAuth,
|
|
69
|
-
parameters,
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
const expiresAt = new Date(Date.now() + PAR_EXPIRES_IN)
|
|
73
|
-
const requestId = await generateRequestId()
|
|
74
|
-
|
|
75
|
-
await this.store.createRequest(requestId, {
|
|
76
|
-
clientId: client.id,
|
|
77
|
-
clientAuth,
|
|
78
|
-
parameters,
|
|
79
|
-
expiresAt,
|
|
80
|
-
deviceId,
|
|
81
|
-
did: null,
|
|
82
|
-
code: null,
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
const requestUri = encodeRequestUri(requestId)
|
|
86
|
-
return { requestUri, expiresAt, parameters }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
protected async validate(
|
|
90
|
-
client: Client,
|
|
91
|
-
clientAuth: null | ClientAuth,
|
|
92
|
-
parameters: Readonly<OAuthAuthorizationRequestParameters>,
|
|
93
|
-
): Promise<Readonly<OAuthAuthorizationRequestParameters>> {
|
|
94
|
-
// -------------------------------
|
|
95
|
-
// Validate unsupported parameters
|
|
96
|
-
// -------------------------------
|
|
97
|
-
|
|
98
|
-
for (const k of [
|
|
99
|
-
// Known unsupported OIDC parameters
|
|
100
|
-
'claims',
|
|
101
|
-
'id_token_hint',
|
|
102
|
-
'nonce', // note that OIDC "nonce" is redundant with PKCE
|
|
103
|
-
] as const) {
|
|
104
|
-
if (parameters[k] !== undefined) {
|
|
105
|
-
throw new AuthorizationError(parameters, `Unsupported "${k}" parameter`)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// -----------------------
|
|
110
|
-
// Validate against server
|
|
111
|
-
// -----------------------
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
!this.metadata.response_types_supported?.includes(
|
|
115
|
-
parameters.response_type,
|
|
116
|
-
)
|
|
117
|
-
) {
|
|
118
|
-
throw new AuthorizationError(
|
|
119
|
-
parameters,
|
|
120
|
-
`Unsupported response_type "${parameters.response_type}"`,
|
|
121
|
-
'unsupported_response_type',
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
parameters.response_type === 'code' &&
|
|
127
|
-
!this.metadata.grant_types_supported?.includes('authorization_code')
|
|
128
|
-
) {
|
|
129
|
-
throw new AuthorizationError(
|
|
130
|
-
parameters,
|
|
131
|
-
`Unsupported grant_type "authorization_code"`,
|
|
132
|
-
'invalid_request',
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (parameters.authorization_details) {
|
|
137
|
-
for (const detail of parameters.authorization_details) {
|
|
138
|
-
if (
|
|
139
|
-
!this.metadata.authorization_details_types_supported?.includes(
|
|
140
|
-
detail.type,
|
|
141
|
-
)
|
|
142
|
-
) {
|
|
143
|
-
throw new InvalidAuthorizationDetailsError(
|
|
144
|
-
parameters,
|
|
145
|
-
`Unsupported "authorization_details" type "${detail.type}"`,
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// -----------------------
|
|
152
|
-
// Validate against client
|
|
153
|
-
// -----------------------
|
|
154
|
-
|
|
155
|
-
parameters = client.validateRequest(parameters)
|
|
156
|
-
|
|
157
|
-
// -------------------
|
|
158
|
-
// Validate parameters
|
|
159
|
-
// -------------------
|
|
160
|
-
|
|
161
|
-
if (!parameters.redirect_uri) {
|
|
162
|
-
// Should already be ensured by client.validateRequest(). Adding here for
|
|
163
|
-
// clarity & extra safety.
|
|
164
|
-
throw new AuthorizationError(parameters, 'Missing "redirect_uri"')
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#section-1.4.1
|
|
168
|
-
// > The authorization server MAY fully or partially ignore the scope
|
|
169
|
-
// > requested by the client, based on the authorization server policy or
|
|
170
|
-
// > the resource owner's instructions. If the issued access token scope is
|
|
171
|
-
// > different from the one requested by the client, the authorization
|
|
172
|
-
// > server MUST include the scope response parameter in the token response
|
|
173
|
-
// > (Section 3.2.3) to inform the client of the actual scope granted.
|
|
174
|
-
|
|
175
|
-
// Let's make sure the scopes are unique (to reduce the token & storage
|
|
176
|
-
// size).
|
|
177
|
-
const scopes = new Set(parameters.scope?.split(' '))
|
|
178
|
-
|
|
179
|
-
// @NOTE An app requesting a not yet supported list of scopes will need to
|
|
180
|
-
// re-authenticate the user once the scopes are supported. This is due to
|
|
181
|
-
// the fact that the AS does not know how to properly display those scopes
|
|
182
|
-
// to the user, so it cannot properly ask for consent.
|
|
183
|
-
const scope =
|
|
184
|
-
Array.from(scopes).filter(isAtprotoOauthScope).join(' ') || undefined
|
|
185
|
-
parameters = { ...parameters, scope }
|
|
186
|
-
|
|
187
|
-
if (parameters.code_challenge) {
|
|
188
|
-
switch (parameters.code_challenge_method) {
|
|
189
|
-
case undefined:
|
|
190
|
-
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.3
|
|
191
|
-
parameters = { ...parameters, code_challenge_method: 'plain' }
|
|
192
|
-
// falls through
|
|
193
|
-
case 'plain':
|
|
194
|
-
case 'S256':
|
|
195
|
-
break
|
|
196
|
-
default: {
|
|
197
|
-
throw new AuthorizationError(
|
|
198
|
-
parameters,
|
|
199
|
-
`Unsupported code_challenge_method "${parameters.code_challenge_method}"`,
|
|
200
|
-
)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
} else {
|
|
204
|
-
if (parameters.code_challenge_method) {
|
|
205
|
-
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1
|
|
206
|
-
throw new AuthorizationError(
|
|
207
|
-
parameters,
|
|
208
|
-
'code_challenge is required when code_challenge_method is provided',
|
|
209
|
-
)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.2.1
|
|
213
|
-
//
|
|
214
|
-
// > An AS MUST reject requests without a code_challenge from public
|
|
215
|
-
// > clients, and MUST reject such requests from other clients unless
|
|
216
|
-
// > there is reasonable assurance that the client mitigates
|
|
217
|
-
// > authorization code injection in other ways. See Section 7.5.1 for
|
|
218
|
-
// > details.
|
|
219
|
-
//
|
|
220
|
-
// > [...] In the specific deployment and the specific request, there is
|
|
221
|
-
// > reasonable assurance by the authorization server that the client
|
|
222
|
-
// > implements the OpenID Connect nonce mechanism properly.
|
|
223
|
-
//
|
|
224
|
-
// atproto does not implement the OpenID Connect nonce mechanism, so we
|
|
225
|
-
// require the use of PKCE for all clients.
|
|
226
|
-
|
|
227
|
-
throw new AuthorizationError(parameters, 'Use of PKCE is required')
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// -----------------
|
|
231
|
-
// atproto extension
|
|
232
|
-
// -----------------
|
|
233
|
-
|
|
234
|
-
if (parameters.response_type !== 'code') {
|
|
235
|
-
throw new AuthorizationError(
|
|
236
|
-
parameters,
|
|
237
|
-
'atproto only supports the "code" response_type',
|
|
238
|
-
)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (!scopes.has('atproto')) {
|
|
242
|
-
throw new InvalidScopeError(parameters, 'The "atproto" scope is required')
|
|
243
|
-
} else if (scopes.has('openid')) {
|
|
244
|
-
throw new InvalidScopeError(
|
|
245
|
-
parameters,
|
|
246
|
-
'OpenID Connect is not compatible with atproto',
|
|
247
|
-
)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (parameters.code_challenge_method !== 'S256') {
|
|
251
|
-
throw new AuthorizationError(
|
|
252
|
-
parameters,
|
|
253
|
-
'atproto requires use of "S256" code_challenge_method',
|
|
254
|
-
)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// atproto extension: if the client is not trusted, and not authenticated,
|
|
258
|
-
// force users to consent to authorization requests. We do this to avoid
|
|
259
|
-
// unauthenticated clients from being able to silently re-authenticate
|
|
260
|
-
// users.
|
|
261
|
-
if (
|
|
262
|
-
!client.info.isTrusted &&
|
|
263
|
-
!client.info.isFirstParty &&
|
|
264
|
-
client.metadata.token_endpoint_auth_method === 'none'
|
|
265
|
-
) {
|
|
266
|
-
if (parameters.prompt === 'none') {
|
|
267
|
-
throw new ConsentRequiredError(
|
|
268
|
-
parameters,
|
|
269
|
-
'Public clients are not allowed to use silent-sign-on',
|
|
270
|
-
)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// force "consent" for unauthenticated third party clients, unless they
|
|
274
|
-
// are trying to create accounts:
|
|
275
|
-
if (parameters.prompt !== 'create') {
|
|
276
|
-
parameters = { ...parameters, prompt: 'consent' }
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// atproto extension: ensure that the login_hint is a valid handle or DID
|
|
281
|
-
// @NOTE we to allow invalid case here, which is not spec'd anywhere.
|
|
282
|
-
const hint = parameters.login_hint?.toLowerCase()
|
|
283
|
-
if (hint) {
|
|
284
|
-
if (!isAtprotoDid(hint) && !isValidHandle(hint)) {
|
|
285
|
-
throw new AuthorizationError(parameters, `Invalid login_hint "${hint}"`)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// @TODO: ensure that the account actually exists on this server (there is
|
|
289
|
-
// no point in showing the UI to the user if the account does not exist).
|
|
290
|
-
|
|
291
|
-
// Update the parameters to ensure the right case is used
|
|
292
|
-
parameters = { ...parameters, login_hint: hint }
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Make sure that every nsid in the scope resolves to a valid permission set
|
|
296
|
-
// lexicon
|
|
297
|
-
if (parameters.scope) {
|
|
298
|
-
try {
|
|
299
|
-
await this.lexiconManager.getPermissionSetsFromScope(parameters.scope)
|
|
300
|
-
} catch (err) {
|
|
301
|
-
// Parse expected errors
|
|
302
|
-
if (err instanceof LexResolverError) {
|
|
303
|
-
throw new AuthorizationError(
|
|
304
|
-
parameters,
|
|
305
|
-
err.message,
|
|
306
|
-
'invalid_scope',
|
|
307
|
-
err,
|
|
308
|
-
)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Unexpected error
|
|
312
|
-
throw err
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return parameters
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Reads the {@link ClientId} associated with a request URI without any of
|
|
321
|
-
* the validation or side-effects performed by {@link RequestManager.get}
|
|
322
|
-
*
|
|
323
|
-
* Returns `undefined` when no such request exists.
|
|
324
|
-
*/
|
|
325
|
-
async peekClientId(requestUri: RequestUri): Promise<ClientId | undefined> {
|
|
326
|
-
const requestId = decodeRequestUri(requestUri)
|
|
327
|
-
const data = await this.store.readRequest(requestId)
|
|
328
|
-
return data?.clientId
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async get(requestUri: RequestUri, deviceId?: DeviceId, clientId?: ClientId) {
|
|
332
|
-
const requestId = decodeRequestUri(requestUri)
|
|
333
|
-
|
|
334
|
-
const data = await this.store.readRequest(requestId)
|
|
335
|
-
if (!data) throw new InvalidRequestError('Unknown request_uri')
|
|
336
|
-
|
|
337
|
-
const updates: UpdateRequestData = {}
|
|
338
|
-
|
|
339
|
-
try {
|
|
340
|
-
if (data.did || data.code) {
|
|
341
|
-
// If an account was linked to the request, the next step is to exchange
|
|
342
|
-
// the code for a token.
|
|
343
|
-
throw new AccessDeniedError(
|
|
344
|
-
data.parameters,
|
|
345
|
-
'This request was already authorized',
|
|
346
|
-
)
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (data.expiresAt < new Date()) {
|
|
350
|
-
throw new AccessDeniedError(data.parameters, 'This request has expired')
|
|
351
|
-
} else {
|
|
352
|
-
updates.expiresAt = new Date(
|
|
353
|
-
Date.now() + AUTHORIZATION_INACTIVITY_TIMEOUT,
|
|
354
|
-
)
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (clientId != null && data.clientId !== clientId) {
|
|
358
|
-
throw new AccessDeniedError(
|
|
359
|
-
data.parameters,
|
|
360
|
-
'This request was initiated for another client',
|
|
361
|
-
)
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (deviceId != null) {
|
|
365
|
-
if (!data.deviceId) {
|
|
366
|
-
updates.deviceId = deviceId
|
|
367
|
-
} else if (data.deviceId !== deviceId) {
|
|
368
|
-
throw new AccessDeniedError(
|
|
369
|
-
data.parameters,
|
|
370
|
-
'This request was initiated from another device',
|
|
371
|
-
)
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
} catch (err) {
|
|
375
|
-
await this.store.deleteRequest(requestId)
|
|
376
|
-
throw err
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (Object.keys(updates).length > 0) {
|
|
380
|
-
await this.store.updateRequest(requestId, updates)
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return {
|
|
384
|
-
requestUri,
|
|
385
|
-
expiresAt: updates.expiresAt || data.expiresAt,
|
|
386
|
-
parameters: data.parameters,
|
|
387
|
-
clientId: data.clientId,
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
async setAuthorized(
|
|
392
|
-
requestUri: RequestUri,
|
|
393
|
-
client: Client,
|
|
394
|
-
account: Account,
|
|
395
|
-
deviceId: DeviceId,
|
|
396
|
-
deviceMetadata: RequestMetadata,
|
|
397
|
-
scopeOverride?: string,
|
|
398
|
-
): Promise<Code> {
|
|
399
|
-
const requestId = decodeRequestUri(requestUri)
|
|
400
|
-
|
|
401
|
-
const data = await this.store.readRequest(requestId)
|
|
402
|
-
if (!data) throw new InvalidRequestError('Unknown request_uri')
|
|
403
|
-
|
|
404
|
-
let { parameters } = data
|
|
405
|
-
|
|
406
|
-
try {
|
|
407
|
-
if (account.deactivated) {
|
|
408
|
-
throw new AccessDeniedError(
|
|
409
|
-
parameters,
|
|
410
|
-
'This account has been deactivated',
|
|
411
|
-
)
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (data.expiresAt < new Date()) {
|
|
415
|
-
throw new AccessDeniedError(parameters, 'This request has expired')
|
|
416
|
-
}
|
|
417
|
-
if (!data.deviceId) {
|
|
418
|
-
throw new AccessDeniedError(
|
|
419
|
-
parameters,
|
|
420
|
-
'This request was not initiated',
|
|
421
|
-
)
|
|
422
|
-
}
|
|
423
|
-
if (data.deviceId !== deviceId) {
|
|
424
|
-
throw new AccessDeniedError(
|
|
425
|
-
parameters,
|
|
426
|
-
'This request was initiated from another device',
|
|
427
|
-
)
|
|
428
|
-
}
|
|
429
|
-
if (data.did || data.code) {
|
|
430
|
-
throw new AccessDeniedError(
|
|
431
|
-
parameters,
|
|
432
|
-
'This request was already authorized',
|
|
433
|
-
)
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// If a new scope value is provided, update the parameters by ensuring
|
|
437
|
-
// that every existing scope in the parameters is also present in the
|
|
438
|
-
// override value. This allows the user to remove scopes from the request,
|
|
439
|
-
// but not to add new ones.
|
|
440
|
-
if (scopeOverride != null) {
|
|
441
|
-
const allowedScopes = new Set(scopeOverride.split(' '))
|
|
442
|
-
const existingScopes = parameters.scope?.split(' ')
|
|
443
|
-
|
|
444
|
-
// Compute the intersection of the existing scopes and the overrides.
|
|
445
|
-
const newScopes = existingScopes?.filter((s) => allowedScopes.has(s))
|
|
446
|
-
|
|
447
|
-
// Validate: make sure the new scopes are valid
|
|
448
|
-
if (!newScopes?.includes('atproto')) {
|
|
449
|
-
throw new AccessDeniedError(
|
|
450
|
-
parameters,
|
|
451
|
-
'The "atproto" scope is required',
|
|
452
|
-
)
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
parameters = { ...parameters, scope: newScopes.join(' ') }
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Only response_type=code is supported
|
|
459
|
-
const code = await generateCode()
|
|
460
|
-
|
|
461
|
-
// Bind the request to the account, preventing it from being used again.
|
|
462
|
-
await this.store.updateRequest(requestId, {
|
|
463
|
-
did: account.did,
|
|
464
|
-
code,
|
|
465
|
-
// Allow the client to exchange the code for a token within the next 60 seconds.
|
|
466
|
-
expiresAt: new Date(Date.now() + AUTHORIZATION_INACTIVITY_TIMEOUT),
|
|
467
|
-
parameters,
|
|
468
|
-
})
|
|
469
|
-
|
|
470
|
-
await this.hooks.onAuthorized?.call(null, {
|
|
471
|
-
client,
|
|
472
|
-
account,
|
|
473
|
-
parameters,
|
|
474
|
-
deviceId,
|
|
475
|
-
deviceMetadata,
|
|
476
|
-
requestId,
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
return code
|
|
480
|
-
} catch (err) {
|
|
481
|
-
await this.store.deleteRequest(requestId)
|
|
482
|
-
throw err
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* @note If this method throws an error, any token previously generated from
|
|
488
|
-
* the same `code` **must** me revoked.
|
|
489
|
-
*/
|
|
490
|
-
public async consumeCode(code: Code): Promise<RequestDataAuthorized> {
|
|
491
|
-
const result = await this.store.consumeRequestCode(code)
|
|
492
|
-
if (!result) throw new InvalidGrantError('Invalid code')
|
|
493
|
-
|
|
494
|
-
const { requestId, data } = result
|
|
495
|
-
|
|
496
|
-
// Fool-proofing the store implementation against code replay attacks (in
|
|
497
|
-
// case consumeRequestCode() does not delete the request).
|
|
498
|
-
if (NODE_ENV !== 'production') {
|
|
499
|
-
const result = await this.store.readRequest(requestId)
|
|
500
|
-
if (result) {
|
|
501
|
-
throw new Error('Invalid store implementation: request not deleted')
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
if (!isRequestDataAuthorized(data) || data.code !== code) {
|
|
506
|
-
// Should never happen: maybe the store implementation is faulty ?
|
|
507
|
-
throw new Error('Unexpected request state')
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (data.expiresAt < new Date()) {
|
|
511
|
-
throw new InvalidGrantError('This code has expired')
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
return data
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
async delete(requestUri: RequestUri): Promise<void> {
|
|
518
|
-
const requestId = decodeRequestUri(requestUri)
|
|
519
|
-
await this.store.deleteRequest(requestId)
|
|
520
|
-
}
|
|
521
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { InvalidGrantError } from '../errors/invalid-grant-error.js'
|
|
2
|
-
import { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'
|
|
3
|
-
import { Code } from './code.js'
|
|
4
|
-
import { RequestData } from './request-data.js'
|
|
5
|
-
import { RequestId } from './request-id.js'
|
|
6
|
-
|
|
7
|
-
// Export all types needed to implement the RequestStore interface
|
|
8
|
-
export * from './code.js'
|
|
9
|
-
export * from './request-data.js'
|
|
10
|
-
export * from './request-id.js'
|
|
11
|
-
export type { Awaitable }
|
|
12
|
-
|
|
13
|
-
export type UpdateRequestData = Pick<
|
|
14
|
-
Partial<RequestData>,
|
|
15
|
-
'did' | 'code' | 'deviceId' | 'expiresAt' | 'parameters'
|
|
16
|
-
>
|
|
17
|
-
|
|
18
|
-
export type FoundRequestResult = {
|
|
19
|
-
requestId: RequestId
|
|
20
|
-
data: RequestData
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export { InvalidGrantError }
|
|
24
|
-
|
|
25
|
-
export interface RequestStore {
|
|
26
|
-
createRequest(requestId: RequestId, data: RequestData): Awaitable<void>
|
|
27
|
-
/**
|
|
28
|
-
* Note that expired requests **can** be returned to yield a different error
|
|
29
|
-
* message than if the request was not found.
|
|
30
|
-
*/
|
|
31
|
-
readRequest(requestId: RequestId): Awaitable<RequestData | null>
|
|
32
|
-
updateRequest(requestId: RequestId, data: UpdateRequestData): Awaitable<void>
|
|
33
|
-
deleteRequest(requestId: RequestId): void | Awaitable<void>
|
|
34
|
-
/**
|
|
35
|
-
* @note it is **IMPORTANT** that this method prevents concurrent retrieval of
|
|
36
|
-
* the same code. If two requests are made with the same code, only one of
|
|
37
|
-
* them should succeed and return the request data.
|
|
38
|
-
*
|
|
39
|
-
* @throws {InvalidGrantError} - When the request is not found or has expired
|
|
40
|
-
* (allows to provide an error message instead of returning `null`).
|
|
41
|
-
*/
|
|
42
|
-
consumeRequestCode(code: Code): Awaitable<FoundRequestResult | null>
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const isRequestStore = buildInterfaceChecker<RequestStore>([
|
|
46
|
-
'consumeRequestCode',
|
|
47
|
-
'createRequest',
|
|
48
|
-
'deleteRequest',
|
|
49
|
-
'readRequest',
|
|
50
|
-
'updateRequest',
|
|
51
|
-
])
|
|
52
|
-
|
|
53
|
-
export function asRequestStore<V extends Partial<RequestStore>>(
|
|
54
|
-
implementation?: V,
|
|
55
|
-
): V & RequestStore {
|
|
56
|
-
if (!implementation || !isRequestStore(implementation)) {
|
|
57
|
-
throw new Error('Invalid RequestStore implementation')
|
|
58
|
-
}
|
|
59
|
-
return implementation
|
|
60
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { InvalidRequestError } from '../errors/invalid-request-error.js'
|
|
3
|
-
import { formatError } from '../lib/util/error.js'
|
|
4
|
-
import { RequestId, requestIdSchema } from './request-id.js'
|
|
5
|
-
|
|
6
|
-
export const REQUEST_URI_PREFIX = 'urn:ietf:params:oauth:request_uri:'
|
|
7
|
-
|
|
8
|
-
export const requestUriSchema = z
|
|
9
|
-
.string()
|
|
10
|
-
.refinement(
|
|
11
|
-
(data): data is `${typeof REQUEST_URI_PREFIX}${RequestId}` =>
|
|
12
|
-
data.startsWith(REQUEST_URI_PREFIX) &&
|
|
13
|
-
requestIdSchema.safeParse(decodeRequestUri(data as any)).success,
|
|
14
|
-
{
|
|
15
|
-
code: z.ZodIssueCode.custom,
|
|
16
|
-
message: 'Invalid request_uri format',
|
|
17
|
-
},
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
export type RequestUri = z.infer<typeof requestUriSchema>
|
|
21
|
-
|
|
22
|
-
export function encodeRequestUri(requestId: RequestId): RequestUri {
|
|
23
|
-
return `${REQUEST_URI_PREFIX}${encodeURIComponent(requestId) as RequestId}`
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function decodeRequestUri(requestUri: RequestUri): RequestId {
|
|
27
|
-
const requestIdEnc = requestUri.slice(REQUEST_URI_PREFIX.length)
|
|
28
|
-
return decodeURIComponent(requestIdEnc) as RequestId
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function parseRequestUri(
|
|
32
|
-
requestUri: string,
|
|
33
|
-
parseParams?: { path?: (string | number)[] },
|
|
34
|
-
): RequestUri {
|
|
35
|
-
const parseResult = requestUriSchema.safeParse(requestUri, parseParams)
|
|
36
|
-
if (!parseResult.success) {
|
|
37
|
-
const err = parseResult.error
|
|
38
|
-
const msg = formatError(err, 'Invalid "request_uri" query parameter')
|
|
39
|
-
throw new InvalidRequestError(msg, err)
|
|
40
|
-
}
|
|
41
|
-
return parseResult.data
|
|
42
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { OAuthTokenType } from '@atproto/oauth-types'
|
|
2
|
-
import { Code } from '../request/code.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @note `iss` and `state` will be added from the
|
|
6
|
-
* {@link AuthorizationResultRedirect} object.
|
|
7
|
-
*/
|
|
8
|
-
export type AuthorizationRedirectParameters =
|
|
9
|
-
| {
|
|
10
|
-
// iss: string
|
|
11
|
-
// state?: string
|
|
12
|
-
code: Code
|
|
13
|
-
id_token?: string
|
|
14
|
-
access_token?: string
|
|
15
|
-
token_type?: OAuthTokenType
|
|
16
|
-
expires_in?: string
|
|
17
|
-
}
|
|
18
|
-
| {
|
|
19
|
-
// iss: string
|
|
20
|
-
// state?: string
|
|
21
|
-
error: string
|
|
22
|
-
error_description?: string
|
|
23
|
-
error_uri?: string
|
|
24
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { LexiconPermissionSet } from '@atproto/lex-document'
|
|
2
|
-
import type { Account, Session } from '@atproto/oauth-provider-api'
|
|
3
|
-
import type { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'
|
|
4
|
-
import type { Client } from '../client/client.js'
|
|
5
|
-
import type { RequestUri } from '../request/request-uri.js'
|
|
6
|
-
|
|
7
|
-
export type AuthorizationResultAuthorizePage = {
|
|
8
|
-
issuer: string
|
|
9
|
-
client: Client
|
|
10
|
-
parameters: OAuthAuthorizationRequestParameters
|
|
11
|
-
permissionSets: Map<string, LexiconPermissionSet>
|
|
12
|
-
|
|
13
|
-
requestUri: RequestUri
|
|
14
|
-
sessions: readonly Session[]
|
|
15
|
-
selectedDid?: Account['did']
|
|
16
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'
|
|
2
|
-
import { AuthorizationRedirectParameters } from './authorization-redirect-parameters.js'
|
|
3
|
-
|
|
4
|
-
export type AuthorizationResultRedirect = {
|
|
5
|
-
issuer: string
|
|
6
|
-
parameters: OAuthAuthorizationRequestParameters
|
|
7
|
-
redirect: AuthorizationRedirectParameters
|
|
8
|
-
}
|