@atproto/oauth-provider 0.19.6 → 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.
Files changed (165) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/errors/error-parser.js +5 -5
  3. package/dist/errors/error-parser.js.map +1 -1
  4. package/package.json +22 -18
  5. package/src/access-token/access-token-mode.ts +0 -4
  6. package/src/account/account-manager.ts +0 -591
  7. package/src/account/account-store.ts +0 -305
  8. package/src/account/sign-in-data.ts +0 -15
  9. package/src/account/sign-up-input.ts +0 -20
  10. package/src/client/client-auth.ts +0 -64
  11. package/src/client/client-data.ts +0 -9
  12. package/src/client/client-id.ts +0 -4
  13. package/src/client/client-info.ts +0 -13
  14. package/src/client/client-manager.ts +0 -772
  15. package/src/client/client-store.ts +0 -35
  16. package/src/client/client-utils.ts +0 -37
  17. package/src/client/client.ts +0 -394
  18. package/src/constants.ts +0 -80
  19. package/src/customization/branding.ts +0 -12
  20. package/src/customization/build-customization-css.ts +0 -55
  21. package/src/customization/build-customization-data.ts +0 -24
  22. package/src/customization/colors.ts +0 -48
  23. package/src/customization/customization.ts +0 -29
  24. package/src/customization/links.ts +0 -10
  25. package/src/device/device-data.ts +0 -11
  26. package/src/device/device-id.ts +0 -27
  27. package/src/device/device-manager.ts +0 -292
  28. package/src/device/device-store.ts +0 -31
  29. package/src/device/session-id.ts +0 -21
  30. package/src/dpop/dpop-manager.ts +0 -214
  31. package/src/dpop/dpop-nonce.ts +0 -121
  32. package/src/dpop/dpop-proof.ts +0 -6
  33. package/src/errors/access-denied-error.ts +0 -12
  34. package/src/errors/account-selection-required-error.ts +0 -12
  35. package/src/errors/authorization-error.ts +0 -45
  36. package/src/errors/consent-required-error.ts +0 -12
  37. package/src/errors/error-parser.ts +0 -147
  38. package/src/errors/handle-unavailable-error.ts +0 -23
  39. package/src/errors/invalid-authorization-details-error.ts +0 -27
  40. package/src/errors/invalid-client-error.ts +0 -20
  41. package/src/errors/invalid-client-id-error.ts +0 -31
  42. package/src/errors/invalid-client-metadata-error.ts +0 -68
  43. package/src/errors/invalid-credentials-error.ts +0 -29
  44. package/src/errors/invalid-dpop-key-binding-error.ts +0 -21
  45. package/src/errors/invalid-dpop-proof-error.ts +0 -13
  46. package/src/errors/invalid-grant-error.ts +0 -21
  47. package/src/errors/invalid-invite-code-error.ts +0 -10
  48. package/src/errors/invalid-redirect-uri-error.ts +0 -17
  49. package/src/errors/invalid-request-error.ts +0 -32
  50. package/src/errors/invalid-scope-error.ts +0 -15
  51. package/src/errors/invalid-token-error.ts +0 -60
  52. package/src/errors/login-required-error.ts +0 -12
  53. package/src/errors/oauth-error.ts +0 -28
  54. package/src/errors/second-authentication-factor-required-error.ts +0 -25
  55. package/src/errors/unauthorized-client-error.ts +0 -20
  56. package/src/errors/use-dpop-nonce-error.ts +0 -32
  57. package/src/errors/www-authenticate-error.ts +0 -64
  58. package/src/index.ts +0 -16
  59. package/src/lexicon/lexicon-data.ts +0 -11
  60. package/src/lexicon/lexicon-getter.ts +0 -55
  61. package/src/lexicon/lexicon-manager.ts +0 -116
  62. package/src/lexicon/lexicon-store.ts +0 -35
  63. package/src/lib/csp/index.ts +0 -101
  64. package/src/lib/hcaptcha.ts +0 -228
  65. package/src/lib/html/README.md +0 -9
  66. package/src/lib/html/build-document.ts +0 -151
  67. package/src/lib/html/escapers.ts +0 -66
  68. package/src/lib/html/html.ts +0 -56
  69. package/src/lib/html/hydration-data.ts +0 -19
  70. package/src/lib/html/index.ts +0 -5
  71. package/src/lib/html/tags.ts +0 -67
  72. package/src/lib/html/util.ts +0 -17
  73. package/src/lib/http/README.md +0 -11
  74. package/src/lib/http/accept.ts +0 -90
  75. package/src/lib/http/context.ts +0 -42
  76. package/src/lib/http/headers.ts +0 -15
  77. package/src/lib/http/index.ts +0 -10
  78. package/src/lib/http/method.ts +0 -18
  79. package/src/lib/http/middleware.ts +0 -169
  80. package/src/lib/http/parser.ts +0 -101
  81. package/src/lib/http/path.ts +0 -82
  82. package/src/lib/http/request.ts +0 -256
  83. package/src/lib/http/response.ts +0 -122
  84. package/src/lib/http/route.ts +0 -60
  85. package/src/lib/http/router.ts +0 -117
  86. package/src/lib/http/security-headers.ts +0 -100
  87. package/src/lib/http/stream.ts +0 -57
  88. package/src/lib/http/types.ts +0 -16
  89. package/src/lib/http/url.ts +0 -23
  90. package/src/lib/nsid.ts +0 -10
  91. package/src/lib/redis.ts +0 -25
  92. package/src/lib/util/authorization-header.ts +0 -28
  93. package/src/lib/util/cast.ts +0 -18
  94. package/src/lib/util/color.ts +0 -168
  95. package/src/lib/util/crypto.ts +0 -32
  96. package/src/lib/util/date.ts +0 -7
  97. package/src/lib/util/error.ts +0 -7
  98. package/src/lib/util/function.ts +0 -39
  99. package/src/lib/util/locale.ts +0 -12
  100. package/src/lib/util/object.ts +0 -23
  101. package/src/lib/util/redirect-uri.ts +0 -46
  102. package/src/lib/util/time.ts +0 -49
  103. package/src/lib/util/type.ts +0 -182
  104. package/src/lib/util/ui8.ts +0 -14
  105. package/src/lib/util/well-known.ts +0 -8
  106. package/src/lib/util/zod-error.ts +0 -26
  107. package/src/lib/write-form-redirect.ts +0 -58
  108. package/src/lib/write-html.ts +0 -70
  109. package/src/metadata/build-metadata.ts +0 -141
  110. package/src/oauth-client.ts +0 -3
  111. package/src/oauth-dpop.ts +0 -2
  112. package/src/oauth-errors.ts +0 -26
  113. package/src/oauth-hooks.ts +0 -519
  114. package/src/oauth-middleware.ts +0 -53
  115. package/src/oauth-provider.ts +0 -1110
  116. package/src/oauth-store.ts +0 -12
  117. package/src/oauth-verifier.ts +0 -260
  118. package/src/replay/replay-manager.ts +0 -51
  119. package/src/replay/replay-store-memory.ts +0 -36
  120. package/src/replay/replay-store-redis.ts +0 -30
  121. package/src/replay/replay-store.ts +0 -44
  122. package/src/request/code.ts +0 -23
  123. package/src/request/request-data.ts +0 -36
  124. package/src/request/request-id.ts +0 -22
  125. package/src/request/request-manager.ts +0 -521
  126. package/src/request/request-store.ts +0 -60
  127. package/src/request/request-uri.ts +0 -42
  128. package/src/result/authorization-redirect-parameters.ts +0 -24
  129. package/src/result/authorization-result-authorize-page.ts +0 -16
  130. package/src/result/authorization-result-redirect.ts +0 -8
  131. package/src/router/assets/assets-manifest.ts +0 -117
  132. package/src/router/assets/assets.ts +0 -124
  133. package/src/router/assets/csrf.ts +0 -79
  134. package/src/router/assets/send-account-page.ts +0 -23
  135. package/src/router/assets/send-authorization-page.ts +0 -42
  136. package/src/router/assets/send-cookie-error-page.ts +0 -21
  137. package/src/router/assets/send-error-page.ts +0 -25
  138. package/src/router/assets/send-redirect.ts +0 -140
  139. package/src/router/create-account-page-middleware.ts +0 -88
  140. package/src/router/create-api-middleware.ts +0 -1051
  141. package/src/router/create-authorization-page-middleware.ts +0 -281
  142. package/src/router/create-oauth-middleware.ts +0 -257
  143. package/src/router/error-handler.ts +0 -6
  144. package/src/router/middleware-options.ts +0 -9
  145. package/src/signer/access-token-payload.ts +0 -25
  146. package/src/signer/api-token-payload.ts +0 -18
  147. package/src/signer/signer.ts +0 -121
  148. package/src/token/refresh-token.ts +0 -30
  149. package/src/token/token-claims.ts +0 -22
  150. package/src/token/token-data.ts +0 -40
  151. package/src/token/token-id.ts +0 -25
  152. package/src/token/token-manager.ts +0 -427
  153. package/src/token/token-store.ts +0 -88
  154. package/src/types/authorization-response-error.ts +0 -27
  155. package/src/types/color-hue.ts +0 -3
  156. package/src/types/email-otp.ts +0 -3
  157. package/src/types/email.ts +0 -26
  158. package/src/types/handle.ts +0 -23
  159. package/src/types/invite-code.ts +0 -4
  160. package/src/types/par-response-error.ts +0 -25
  161. package/src/types/password.ts +0 -4
  162. package/src/types/rgb-color.ts +0 -21
  163. package/tsconfig.build.json +0 -8
  164. package/tsconfig.build.tsbuildinfo +0 -1
  165. package/tsconfig.json +0 -4
@@ -1,35 +0,0 @@
1
- import { OAuthClientMetadata } from '@atproto/oauth-types'
2
- import { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'
3
- import { ClientId } from './client-id.js'
4
-
5
- // Export all types needed to implement the ClientStore interface
6
- export * from './client-data.js'
7
- export * from './client-id.js'
8
- export type { Awaitable, OAuthClientMetadata }
9
-
10
- export interface ClientStore {
11
- findClient(clientId: ClientId): Awaitable<OAuthClientMetadata>
12
- }
13
-
14
- export const isClientStore = buildInterfaceChecker<ClientStore>([
15
- 'findClient', //
16
- ])
17
-
18
- export function ifClientStore<V extends Partial<ClientStore>>(
19
- implementation?: V,
20
- ): (V & ClientStore) | undefined {
21
- if (implementation && isClientStore(implementation)) {
22
- return implementation
23
- }
24
-
25
- return undefined
26
- }
27
-
28
- export function asClientStore<V extends Partial<ClientStore>>(
29
- implementation?: V,
30
- ): V & ClientStore {
31
- const store = ifClientStore(implementation)
32
- if (store) return store
33
-
34
- throw new Error('Invalid ClientStore implementation')
35
- }
@@ -1,37 +0,0 @@
1
- import {
2
- OAuthClientIdDiscoverable,
3
- isLocalHostname,
4
- parseOAuthDiscoverableClientId,
5
- } from '@atproto/oauth-types'
6
- import { InvalidClientIdError } from '../errors/invalid-client-id-error.js'
7
- import { InvalidRedirectUriError } from '../errors/invalid-redirect-uri-error.js'
8
-
9
- export function parseRedirectUri(redirectUri: string): URL {
10
- try {
11
- return new URL(redirectUri)
12
- } catch (err) {
13
- throw InvalidRedirectUriError.from(err)
14
- }
15
- }
16
-
17
- export function parseDiscoverableClientId(
18
- clientId: OAuthClientIdDiscoverable,
19
- ): URL {
20
- try {
21
- const url = parseOAuthDiscoverableClientId(clientId)
22
-
23
- // Extra validation, prevent usage of invalid internet domain names.
24
- if (isLocalHostname(url.hostname)) {
25
- throw new InvalidClientIdError(
26
- "The client_id's TLD must not be a local hostname",
27
- )
28
- }
29
-
30
- return url
31
- } catch (err) {
32
- throw InvalidClientIdError.from(
33
- err,
34
- 'Invalid discoverable client identifier',
35
- )
36
- }
37
- }
@@ -1,394 +0,0 @@
1
- import {
2
- JWTClaimVerificationOptions,
3
- type JWTHeaderParameters,
4
- type JWTPayload,
5
- type JWTVerifyOptions,
6
- type JWTVerifyResult,
7
- type KeyLike,
8
- type ResolvedKey,
9
- UnsecuredJWT,
10
- type UnsecuredResult,
11
- calculateJwkThumbprint,
12
- createLocalJWKSet,
13
- createRemoteJWKSet,
14
- errors,
15
- exportJWK,
16
- jwtVerify,
17
- } from 'jose'
18
- import { Jwks, SignedJwt, UnsignedJwt } from '@atproto/jwk'
19
- import {
20
- CLIENT_ASSERTION_TYPE_JWT_BEARER,
21
- OAuthAuthorizationRequestParameters,
22
- OAuthClientCredentials,
23
- OAuthClientMetadata,
24
- OAuthRedirectUri,
25
- } from '@atproto/oauth-types'
26
- import { CLIENT_ASSERTION_MAX_AGE, JAR_MAX_AGE } from '../constants.js'
27
- import { AuthorizationError } from '../errors/authorization-error.js'
28
- import { InvalidAuthorizationDetailsError } from '../errors/invalid-authorization-details-error.js'
29
- import { InvalidClientError } from '../errors/invalid-client-error.js'
30
- import { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'
31
- import { InvalidRequestError } from '../errors/invalid-request-error.js'
32
- import { InvalidScopeError } from '../errors/invalid-scope-error.js'
33
- import { asArray } from '../lib/util/cast.js'
34
- import { compareRedirectUri } from '../lib/util/redirect-uri.js'
35
- import { Awaitable } from '../lib/util/type.js'
36
- import { ClientAuth } from './client-auth.js'
37
- import { ClientId } from './client-id.js'
38
- import { ClientInfo } from './client-info.js'
39
-
40
- const { JOSEError } = errors
41
-
42
- export class Client {
43
- /**
44
- * @see {@link https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-endpoint-auth-method}
45
- */
46
- static readonly AUTH_METHODS_SUPPORTED = ['none', 'private_key_jwt'] as const
47
-
48
- private readonly keyGetter: (
49
- protectedHeader: JWTHeaderParameters,
50
- ) => Awaitable<KeyLike | Uint8Array>
51
-
52
- constructor(
53
- public readonly id: ClientId,
54
- public readonly metadata: OAuthClientMetadata,
55
- public readonly jwks: undefined | Jwks = metadata.jwks,
56
- public readonly info: ClientInfo,
57
- ) {
58
- // If the remote JWKS content is provided, we don't need to fetch it again.
59
- this.keyGetter =
60
- jwks || !metadata.jwks_uri
61
- ? createLocalJWKSet(jwks || { keys: [] })
62
- : createRemoteJWKSet(new URL(metadata.jwks_uri), {})
63
- }
64
-
65
- /**
66
- * @see {@link https://www.rfc-editor.org/rfc/rfc9101.html#name-request-object-2}
67
- */
68
- public async decodeRequestObject(
69
- jar: SignedJwt | UnsignedJwt,
70
- audience: string,
71
- ) {
72
- // https://www.rfc-editor.org/rfc/rfc9101.html#name-request-object-2
73
- // > If signed, the Authorization Request Object SHOULD contain the Claims
74
- // > iss (issuer) and aud (audience) as members with their semantics being
75
- // > the same as defined in the JWT [RFC7519] specification. The value of
76
- // > aud should be the value of the authorization server (AS) issuer, as
77
- // > defined in RFC 8414 [RFC8414].
78
- try {
79
- // We need to special case the "none" algorithm, as the validation method
80
- // is different for signed and unsigned JWTs.
81
- if (this.metadata.request_object_signing_alg === 'none') {
82
- return await this.jwtVerifyUnsecured(jar, {
83
- audience,
84
- maxTokenAge: JAR_MAX_AGE / 1e3,
85
- allowMissingAudience: true,
86
- allowMissingIssuer: true,
87
- })
88
- }
89
-
90
- return await this.jwtVerify(jar, {
91
- audience,
92
- maxTokenAge: JAR_MAX_AGE / 1e3,
93
- algorithms: this.metadata.request_object_signing_alg
94
- ? [this.metadata.request_object_signing_alg]
95
- : // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
96
- //
97
- // > The default, if omitted, is that any algorithm supported by the OP
98
- // > and the RP MAY be used.
99
- undefined,
100
- })
101
- } catch (err) {
102
- const message =
103
- err instanceof JOSEError
104
- ? `Invalid "request" object: ${err.message}`
105
- : `Invalid "request" object`
106
-
107
- throw new InvalidRequestError(message, err)
108
- }
109
- }
110
-
111
- protected async jwtVerifyUnsecured<PayloadType = JWTPayload>(
112
- token: string,
113
- {
114
- audience,
115
- allowMissingAudience = false,
116
- allowMissingIssuer = false,
117
- ...options
118
- }: Omit<JWTClaimVerificationOptions, 'issuer'> & {
119
- allowMissingIssuer?: boolean
120
- allowMissingAudience?: boolean
121
- } = {},
122
- ): Promise<UnsecuredResult<PayloadType>> {
123
- // jose does not support `allowMissingAudience` and `allowMissingIssuer`
124
- // options, so we need to handle audience and issuer checks manually (see
125
- // bellow).
126
-
127
- const result = UnsecuredJWT.decode<PayloadType>(token, options)
128
-
129
- if (!allowMissingIssuer || result.payload.iss != null) {
130
- if (result.payload.iss !== this.id) {
131
- throw new JOSEError(`Invalid "iss" claim "${result.payload.iss}"`)
132
- }
133
- }
134
-
135
- if (!allowMissingAudience || result.payload.aud != null) {
136
- if (audience != null) {
137
- const payloadAud = asArray(result.payload.aud)
138
- if (!asArray(audience).some((aud) => payloadAud.includes(aud))) {
139
- throw new JOSEError(`Invalid "aud" claim "${result.payload.aud}"`)
140
- }
141
- }
142
- }
143
-
144
- return result
145
- }
146
-
147
- protected async jwtVerify<PayloadType = JWTPayload>(
148
- token: string,
149
- options?: Omit<JWTVerifyOptions, 'issuer'>,
150
- ): Promise<JWTVerifyResult<PayloadType> & ResolvedKey<KeyLike>> {
151
- return jwtVerify<PayloadType>(token, this.keyGetter, {
152
- ...options,
153
- issuer: this.id,
154
- })
155
- }
156
-
157
- /**
158
- * @see {@link https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1}
159
- * @see {@link https://datatracker.ietf.org/doc/html/rfc7523#section-3}
160
- * @see {@link https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-endpoint-auth-method}
161
- */
162
- public async authenticate(
163
- input: OAuthClientCredentials,
164
- checks: {
165
- authorizationServerIdentifier: string
166
- },
167
- ): Promise<ClientAuth> {
168
- const method = this.metadata.token_endpoint_auth_method
169
-
170
- if (method === 'none') {
171
- return { method: 'none' }
172
- }
173
-
174
- if (method === 'private_key_jwt') {
175
- if (!('client_assertion' in input)) {
176
- throw new InvalidRequestError(
177
- `client authentication method "${method}" required a "client_assertion"`,
178
- )
179
- }
180
-
181
- if (input.client_assertion_type === CLIENT_ASSERTION_TYPE_JWT_BEARER) {
182
- // https://www.rfc-editor.org/rfc/rfc7523.html#section-3
183
-
184
- const result = await this.jwtVerify<{
185
- jti: string
186
- exp?: number
187
- }>(input.client_assertion, {
188
- // > 1. The JWT MUST contain an "iss" (issuer) claim that contains a
189
- // > unique identifier for the entity that issued the JWT.
190
- //
191
- // The "issuer" is already checked by jwtVerify()
192
-
193
- // > 2. The JWT MUST contain a "sub" (subject) claim identifying the
194
- // > principal that is the subject of the JWT. Two cases need to be
195
- // > differentiated: [...] For client authentication, the subject
196
- // > MUST be the "client_id" of the OAuth client.
197
- subject: this.id,
198
-
199
- // > 3. The JWT MUST contain an "aud" (audience) claim containing a
200
- // > value that identifies the authorization server as an intended
201
- // > audience. The token endpoint URL of the authorization server
202
- // > MAY be used as a value for an "aud" element to identify the
203
- // > authorization server as an intended audience of the JWT.
204
- audience: checks.authorizationServerIdentifier,
205
-
206
- requiredClaims: [
207
- // > 4. The JWT MUST contain an "exp" (expiration time) claim that
208
- // > limits the time window during which the JWT can be used.
209
- //
210
- // @TODO The presence of "exp" didn't use to be enforced by this
211
- // implementation (or provided by the oauth-client). This is mostly
212
- // fine because "iat" *is* required, but this makes this
213
- // implementation non compliant with RFC7523. We can't just make it
214
- // required as it might break existing clients.
215
-
216
- // 'exp',
217
-
218
- // > 7. The JWT MAY contain a "jti" (JWT ID) claim that provides a
219
- // > unique identifier for the token. The authorization server
220
- // > MAY ensure that JWTs are not replayed by maintaining the set
221
- // > of used "jti" values for the length of time for which the
222
- // > JWT would be considered valid based on the applicable "exp"
223
- // > instant.
224
- 'jti',
225
- ],
226
-
227
- // > 5. The JWT MAY contain an "nbf" (not before) claim that
228
- // > identifies the time before which the token MUST NOT be
229
- // > accepted for processing.
230
- //
231
- // This is already enforced by jose
232
-
233
- // > 6. The JWT MAY contain an "iat" (issued at) claim that identifies
234
- // > the time at which the JWT was issued. Note that the
235
- // > authorization server may reject JWTs with an "iat" claim value
236
- // > that is unreasonably far in the past.
237
- maxTokenAge: CLIENT_ASSERTION_MAX_AGE / 1000,
238
- }).catch((err) => {
239
- const msg =
240
- err instanceof JOSEError
241
- ? `Validation of "client_assertion" failed: ${err.message}`
242
- : `Unable to verify "client_assertion" JWT`
243
-
244
- throw new InvalidClientError(msg, err)
245
- })
246
-
247
- if (!result.protectedHeader.kid) {
248
- throw new InvalidClientError(`"kid" required in client_assertion`)
249
- }
250
-
251
- return {
252
- method: 'private_key_jwt',
253
- jti: result.payload.jti,
254
- exp: result.payload.exp,
255
- jkt: await authJwkThumbprint(result.key),
256
- alg: result.protectedHeader.alg,
257
- kid: result.protectedHeader.kid,
258
- }
259
- }
260
-
261
- throw new InvalidClientError(
262
- `Unsupported client_assertion_type "${input.client_assertion_type}"`,
263
- )
264
- }
265
-
266
- // @ts-expect-error Ensure to keep Client.AUTH_METHODS_SUPPORTED in sync
267
- // with the implementation of this function.
268
- if (Client.AUTH_METHODS_SUPPORTED.includes(method)) {
269
- throw new Error(
270
- `verifyCredentials() should implement all of ${[
271
- Client.AUTH_METHODS_SUPPORTED,
272
- ]}`,
273
- )
274
- }
275
-
276
- throw new InvalidClientMetadataError(
277
- `Unsupported token_endpoint_auth_method "${method}"`,
278
- )
279
- }
280
-
281
- /**
282
- * Validates the request parameters against the client metadata.
283
- */
284
- public validateRequest(
285
- parameters: Readonly<OAuthAuthorizationRequestParameters>,
286
- ): Readonly<OAuthAuthorizationRequestParameters> {
287
- if (parameters.client_id !== this.id) {
288
- throw new AuthorizationError(
289
- parameters,
290
- 'The "client_id" parameter field does not match the value used to authenticate the client',
291
- )
292
- }
293
-
294
- if (parameters.scope !== undefined) {
295
- // Any scope requested by the client must be registered in the client
296
- // metadata.
297
- const declaredScopes = this.metadata.scope?.split(' ')
298
-
299
- if (!declaredScopes) {
300
- throw new InvalidScopeError(
301
- parameters,
302
- 'Client has no declared scopes in its metadata',
303
- )
304
- }
305
-
306
- for (const scope of parameters.scope.split(' ')) {
307
- if (!declaredScopes.includes(scope)) {
308
- throw new InvalidScopeError(
309
- parameters,
310
- `Scope "${scope}" is not declared in the client metadata`,
311
- )
312
- }
313
- }
314
- }
315
-
316
- if (!this.metadata.response_types.includes(parameters.response_type)) {
317
- throw new AuthorizationError(
318
- parameters,
319
- `Invalid response_type "${parameters.response_type}" requested by the client`,
320
- )
321
- }
322
-
323
- if (parameters.response_type.includes('code')) {
324
- if (!this.metadata.grant_types.includes('authorization_code')) {
325
- throw new AuthorizationError(
326
- parameters,
327
- `This client is not allowed to use the "authorization_code" grant type`,
328
- )
329
- }
330
- }
331
-
332
- const { redirect_uri } = parameters
333
- if (redirect_uri) {
334
- if (
335
- !this.metadata.redirect_uris.some((uri) =>
336
- compareRedirectUri(uri, redirect_uri),
337
- )
338
- ) {
339
- throw new AuthorizationError(
340
- parameters,
341
- `Invalid redirect_uri ${redirect_uri}`,
342
- )
343
- }
344
- } else {
345
- const { defaultRedirectUri } = this
346
- if (defaultRedirectUri) {
347
- parameters = { ...parameters, redirect_uri: defaultRedirectUri }
348
- } else {
349
- // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#authorization-request
350
- //
351
- // > "redirect_uri": OPTIONAL if only one redirect URI is registered for
352
- // > this client. REQUIRED if multiple redirect URIs are registered for this
353
- // > client.
354
- throw new AuthorizationError(parameters, 'redirect_uri is required')
355
- }
356
- }
357
-
358
- if (parameters.authorization_details) {
359
- const { authorization_details_types } = this.metadata
360
- if (!authorization_details_types) {
361
- throw new InvalidAuthorizationDetailsError(
362
- parameters,
363
- 'Client Metadata does not declare any "authorization_details"',
364
- )
365
- }
366
-
367
- for (const detail of parameters.authorization_details) {
368
- if (!authorization_details_types?.includes(detail.type)) {
369
- throw new InvalidAuthorizationDetailsError(
370
- parameters,
371
- `Client Metadata does not declare any "authorization_details" of type "${detail.type}"`,
372
- )
373
- }
374
- }
375
- }
376
-
377
- return parameters
378
- }
379
-
380
- get defaultRedirectUri(): OAuthRedirectUri | undefined {
381
- const { redirect_uris } = this.metadata
382
- return redirect_uris.length === 1 ? redirect_uris[0] : undefined
383
- }
384
- }
385
-
386
- export async function authJwkThumbprint(
387
- key: Uint8Array | KeyLike,
388
- ): Promise<string> {
389
- try {
390
- return await calculateJwkThumbprint(await exportJWK(key), 'sha512')
391
- } catch (err) {
392
- throw new InvalidClientError('Unable to compute JWK thumbprint', err)
393
- }
394
- }
package/src/constants.ts DELETED
@@ -1,80 +0,0 @@
1
- // The purpose of the prefix is to provide type safety
2
-
3
- export const DEVICE_ID_PREFIX = 'dev-'
4
- export const DEVICE_ID_BYTES_LENGTH = 16 // 128 bits
5
-
6
- export const SESSION_ID_PREFIX = 'ses-'
7
- export const SESSION_ID_BYTES_LENGTH = 16 // 128 bits - only valid if device id is valid
8
-
9
- export const REFRESH_TOKEN_PREFIX = 'ref-'
10
- export const REFRESH_TOKEN_BYTES_LENGTH = 32 // 256 bits
11
-
12
- export const TOKEN_ID_PREFIX = 'tok-'
13
- export const TOKEN_ID_BYTES_LENGTH = 16 // 128 bits - used as `jti` in JWTs (cannot be forged)
14
-
15
- export const REQUEST_ID_PREFIX = 'req-'
16
- export const REQUEST_ID_BYTES_LENGTH = 16 // 128 bits
17
-
18
- export const CODE_PREFIX = 'cod-'
19
- export const CODE_BYTES_LENGTH = 32
20
-
21
- const SECOND = 1e3
22
- const MINUTE = 60 * SECOND
23
- const HOUR = 60 * MINUTE
24
- const DAY = 24 * HOUR
25
- const WEEK = 7 * DAY
26
- const YEAR = 365.25 * DAY
27
- const MONTH = YEAR / 12
28
-
29
- /** 7 days */
30
- export const AUTHENTICATION_MAX_AGE = 7 * DAY
31
-
32
- /** 15 minutes */
33
- export const EPHEMERAL_SESSION_MAX_AGE = 15 * MINUTE
34
-
35
- /** 60 minutes */
36
- export const TOKEN_MAX_AGE = 60 * MINUTE
37
-
38
- /** 5 minutes */
39
- export const AUTHORIZATION_INACTIVITY_TIMEOUT = 5 * MINUTE
40
-
41
- /** 2 week */
42
- export const PUBLIC_CLIENT_SESSION_LIFETIME = 2 * WEEK
43
-
44
- /** @see {@link PUBLIC_CLIENT_SESSION_LIFETIME} */
45
- export const PUBLIC_CLIENT_REFRESH_LIFETIME = PUBLIC_CLIENT_SESSION_LIFETIME
46
-
47
- /** 2 years */
48
- export const CONFIDENTIAL_CLIENT_SESSION_LIFETIME = 2 * YEAR
49
-
50
- /** 3 months */
51
- export const CONFIDENTIAL_CLIENT_REFRESH_LIFETIME = 3 * MONTH
52
-
53
- /** 5 minutes */
54
- export const PAR_EXPIRES_IN = 5 * MINUTE
55
-
56
- /**
57
- * 59 seconds (should be less than a minute)
58
- *
59
- * > "A general guidance for the validity time would be less than a minute."
60
- *
61
- * @see {@link https://datatracker.ietf.org/doc/html/rfc9101#section-10.2 | JWT-Secured Authorization Request (JAR) - Section 10.2 (d)}
62
- */
63
- export const JAR_MAX_AGE = 59 * SECOND
64
-
65
- /** 1 minute */
66
- export const CLIENT_ASSERTION_MAX_AGE = 1 * MINUTE
67
-
68
- /** 3 minutes */
69
- export const DPOP_NONCE_MAX_AGE = 3 * MINUTE
70
-
71
- /** 5 seconds */
72
- export const SESSION_FIXATION_MAX_AGE = 5 * SECOND
73
-
74
- /** 1 day */
75
- export const CODE_CHALLENGE_REPLAY_TIMEFRAME = 1 * DAY
76
-
77
- /** 5 minutes */
78
- export const LEXICON_REFRESH_FREQUENCY = 5 * MINUTE
79
-
80
- export const NODE_ENV = process.env.NODE_ENV || 'production'
@@ -1,12 +0,0 @@
1
- import { z } from 'zod'
2
- import { colorsSchema } from './colors.js'
3
- import { linksSchema } from './links.js'
4
-
5
- export const brandingSchema = z.object({
6
- name: z.string().optional(),
7
- logo: z.string().url().optional(),
8
- colors: colorsSchema.optional(),
9
- links: z.array(linksSchema).optional(),
10
- })
11
- export type BrandingInput = z.input<typeof brandingSchema>
12
- export type Branding = z.infer<typeof brandingSchema>
@@ -1,55 +0,0 @@
1
- import {
2
- RgbColor,
3
- extractHue,
4
- hslToRgb,
5
- pickContrastColor,
6
- } from '../lib/util/color.js'
7
- import { Branding } from './branding.js'
8
- import { COLOR_NAMES } from './colors.js'
9
- import { Customization } from './customization.js'
10
-
11
- export function buildCustomizationCss({
12
- branding,
13
- }: Customization): undefined | string {
14
- const vars = Array.from(buildCustomizationVars(branding))
15
- if (vars.length) return `:root { ${vars.join(' ')} }`
16
- }
17
-
18
- function* buildCustomizationVars(branding?: Branding): Generator<string> {
19
- if (branding?.colors) {
20
- const contrastSaturation = branding.colors.contrastSaturation ?? 30
21
- yield `--contrast-sat: ${contrastSaturation.toFixed(2)}%;`
22
-
23
- const contrastLight: RgbColor =
24
- branding.colors.light ??
25
- // Corresponds to color-contrast-975
26
- hslToRgb({
27
- h: branding.colors.primaryHue ?? 0,
28
- s: contrastSaturation / 100,
29
- l: 0.07,
30
- })
31
- const contrastDark: RgbColor =
32
- branding.colors.dark ??
33
- // Corresponds to color-contrast-25
34
- hslToRgb({
35
- h: branding.colors.primaryHue ?? 0,
36
- s: contrastSaturation / 100,
37
- l: 0.953,
38
- })
39
-
40
- for (const name of COLOR_NAMES) {
41
- const value = branding.colors[name]
42
- if (!value) continue // Skip missing colors
43
-
44
- const contrast =
45
- branding.colors[`${name}Contrast`] ??
46
- pickContrastColor(value, contrastLight, contrastDark)
47
-
48
- const hue = branding.colors[`${name}Hue`] ?? extractHue(value)
49
-
50
- yield `--branding-color-${name}: ${value.r} ${value.g} ${value.b};`
51
- yield `--branding-color-${name}-contrast: ${contrast.r} ${contrast.g} ${contrast.b};`
52
- yield `--branding-color-${name}-hue: ${hue};`
53
- }
54
- }
55
- }
@@ -1,24 +0,0 @@
1
- import type { CustomizationData } from '@atproto/oauth-provider-api'
2
- import type { Customization } from './customization.js'
3
-
4
- export function buildCustomizationData({
5
- branding,
6
- availableUserDomains,
7
- inviteCodeRequired,
8
- show2FaWarningOnEmailUpdate,
9
- hcaptcha,
10
- }: Customization): CustomizationData {
11
- // @NOTE the front end does not need colors here as they will be injected as
12
- // CSS variables.
13
- // @NOTE We only copy the values explicitly needed to avoid leaking sensitive
14
- // data (in case the caller passed more than what we expect).
15
- return {
16
- availableUserDomains,
17
- inviteCodeRequired,
18
- show2FaWarningOnEmailUpdate,
19
- hcaptchaSiteKey: hcaptcha?.siteKey,
20
- name: branding?.name,
21
- logo: branding?.logo,
22
- links: branding?.links,
23
- }
24
- }
@@ -1,48 +0,0 @@
1
- import { z } from 'zod'
2
- import { colorHueSchema } from '../types/color-hue.js'
3
- import { rgbColorSchema } from '../types/rgb-color.js'
4
-
5
- export const COLOR_NAMES = [
6
- 'primary',
7
- 'error',
8
- 'warning',
9
- 'info',
10
- 'success',
11
- ] as const
12
- export type ColorName = (typeof COLOR_NAMES)[number]
13
-
14
- export const colorsSchema = z
15
- .object({
16
- // The "light" and "dark" colors are used as default for unspecified
17
- // contrast colors. The color that has the highest contrast ratio with the
18
- // color base will be used. e.G. If "primary" is specified but
19
- // "primaryContrast" is not, then the contrast color will be either "light"
20
- // or "dark" depending on which one has the highest contrast ratio with
21
- // "primary".
22
- light: rgbColorSchema.optional(),
23
- dark: rgbColorSchema.optional(),
24
-
25
- // The "contrastSaturation" is used to compute the saturation of the
26
- // "contrast" color. The "contrast" color is a (dynamic) color derived from
27
- // the "primaryHue" color with the specified saturation and a variable
28
- // lightness. "color-contrast-900" is used for default text, while
29
- // "color-contrast-0" is used for the page background.
30
- contrastSaturation: z.number().min(0).max(100).optional(),
31
- })
32
- .extend(
33
- Object.fromEntries(
34
- COLOR_NAMES.map((name) => [name, rgbColorSchema.optional()]),
35
- ) as Record<ColorName, z.ZodOptional<typeof rgbColorSchema>>,
36
- )
37
- .extend(
38
- Object.fromEntries(
39
- COLOR_NAMES.map((name) => [`${name}Contrast`, rgbColorSchema.optional()]),
40
- ) as Record<`${ColorName}Contrast`, z.ZodOptional<typeof rgbColorSchema>>,
41
- )
42
- .extend(
43
- Object.fromEntries(
44
- COLOR_NAMES.map((name) => [`${name}Hue`, colorHueSchema.optional()]),
45
- ) as Record<`${ColorName}Hue`, z.ZodOptional<typeof colorHueSchema>>,
46
- )
47
-
48
- export type Colors = z.infer<typeof colorsSchema>