@atproto/oauth-provider 0.15.16 → 0.16.1

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 (81) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/account/account-manager.d.ts +2 -1
  3. package/dist/account/account-manager.d.ts.map +1 -1
  4. package/dist/account/account-manager.js +45 -4
  5. package/dist/account/account-manager.js.map +1 -1
  6. package/dist/account/account-store.d.ts +10 -4
  7. package/dist/account/account-store.d.ts.map +1 -1
  8. package/dist/account/account-store.js +2 -1
  9. package/dist/account/account-store.js.map +1 -1
  10. package/dist/customization/branding.d.ts +18 -1
  11. package/dist/customization/branding.d.ts.map +1 -1
  12. package/dist/customization/build-customization-css.d.ts.map +1 -1
  13. package/dist/customization/build-customization-css.js +16 -2
  14. package/dist/customization/build-customization-css.js.map +1 -1
  15. package/dist/customization/build-customization-data.d.ts +2 -2
  16. package/dist/customization/build-customization-data.d.ts.map +1 -1
  17. package/dist/customization/build-customization-data.js.map +1 -1
  18. package/dist/customization/colors.d.ts +11 -2
  19. package/dist/customization/colors.d.ts.map +1 -1
  20. package/dist/customization/colors.js +19 -1
  21. package/dist/customization/colors.js.map +1 -1
  22. package/dist/customization/customization.d.ts +26 -1
  23. package/dist/customization/customization.d.ts.map +1 -1
  24. package/dist/errors/invalid-credentials-error.d.ts +24 -0
  25. package/dist/errors/invalid-credentials-error.d.ts.map +1 -0
  26. package/dist/errors/invalid-credentials-error.js +30 -0
  27. package/dist/errors/invalid-credentials-error.js.map +1 -0
  28. package/dist/lib/http/router.d.ts +2 -1
  29. package/dist/lib/http/router.d.ts.map +1 -1
  30. package/dist/lib/http/router.js +1 -1
  31. package/dist/lib/http/router.js.map +1 -1
  32. package/dist/lib/util/color.d.ts +3 -0
  33. package/dist/lib/util/color.d.ts.map +1 -1
  34. package/dist/lib/util/color.js +33 -1
  35. package/dist/lib/util/color.js.map +1 -1
  36. package/dist/oauth-errors.d.ts +1 -0
  37. package/dist/oauth-errors.d.ts.map +1 -1
  38. package/dist/oauth-errors.js +1 -0
  39. package/dist/oauth-errors.js.map +1 -1
  40. package/dist/oauth-hooks.d.ts +40 -1
  41. package/dist/oauth-hooks.d.ts.map +1 -1
  42. package/dist/oauth-hooks.js +3 -1
  43. package/dist/oauth-hooks.js.map +1 -1
  44. package/dist/oauth-provider.d.ts.map +1 -1
  45. package/dist/oauth-provider.js +6 -11
  46. package/dist/oauth-provider.js.map +1 -1
  47. package/dist/request/request-manager.d.ts +7 -0
  48. package/dist/request/request-manager.d.ts.map +1 -1
  49. package/dist/request/request-manager.js +11 -0
  50. package/dist/request/request-manager.js.map +1 -1
  51. package/dist/result/authorization-result-authorize-page.d.ts +2 -1
  52. package/dist/result/authorization-result-authorize-page.d.ts.map +1 -1
  53. package/dist/result/authorization-result-authorize-page.js.map +1 -1
  54. package/dist/router/assets/assets.d.ts +1 -2
  55. package/dist/router/assets/assets.d.ts.map +1 -1
  56. package/dist/router/assets/assets.js +20 -12
  57. package/dist/router/assets/assets.js.map +1 -1
  58. package/dist/router/assets/send-authorization-page.d.ts.map +1 -1
  59. package/dist/router/assets/send-authorization-page.js +1 -0
  60. package/dist/router/assets/send-authorization-page.js.map +1 -1
  61. package/dist/router/create-api-middleware.d.ts.map +1 -1
  62. package/dist/router/create-api-middleware.js +11 -7
  63. package/dist/router/create-api-middleware.js.map +1 -1
  64. package/package.json +10 -11
  65. package/src/account/account-manager.ts +49 -6
  66. package/src/account/account-store.ts +10 -2
  67. package/src/customization/build-customization-css.ts +25 -3
  68. package/src/customization/build-customization-data.ts +2 -2
  69. package/src/customization/colors.ts +20 -1
  70. package/src/errors/invalid-credentials-error.ts +29 -0
  71. package/src/lib/http/router.ts +7 -3
  72. package/src/lib/util/color.ts +37 -1
  73. package/src/oauth-errors.ts +1 -0
  74. package/src/oauth-hooks.ts +42 -0
  75. package/src/oauth-provider.ts +7 -13
  76. package/src/request/request-manager.ts +12 -0
  77. package/src/result/authorization-result-authorize-page.ts +2 -1
  78. package/src/router/assets/assets.ts +22 -17
  79. package/src/router/assets/send-authorization-page.ts +1 -0
  80. package/src/router/create-api-middleware.ts +17 -6
  81. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,4 +1,9 @@
1
- import { extractHue, pickContrastColor } from '../lib/util/color.js'
1
+ import {
2
+ RgbColor,
3
+ extractHue,
4
+ hslToRgb,
5
+ pickContrastColor,
6
+ } from '../lib/util/color.js'
2
7
  import { Branding } from './branding.js'
3
8
  import { COLOR_NAMES } from './colors.js'
4
9
  import { Customization } from './customization.js'
@@ -12,8 +17,25 @@ export function buildCustomizationCss({
12
17
 
13
18
  function* buildCustomizationVars(branding?: Branding): Generator<string> {
14
19
  if (branding?.colors) {
15
- const contrastLight = branding.colors.light ?? { r: 255, g: 255, b: 255 }
16
- const contrastDark = branding.colors.dark ?? { r: 0, g: 0, b: 0 }
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
+ })
17
39
 
18
40
  for (const name of COLOR_NAMES) {
19
41
  const value = branding.colors[name]
@@ -1,5 +1,5 @@
1
- import { CustomizationData } from '@atproto/oauth-provider-api'
2
- import { Customization } from './customization.js'
1
+ import type { CustomizationData } from '@atproto/oauth-provider-api'
2
+ import type { Customization } from './customization.js'
3
3
 
4
4
  export function buildCustomizationData({
5
5
  branding,
@@ -2,13 +2,32 @@ import { z } from 'zod'
2
2
  import { colorHueSchema } from '../types/color-hue.js'
3
3
  import { rgbColorSchema } from '../types/rgb-color.js'
4
4
 
5
- export const COLOR_NAMES = ['primary', 'error', 'warning', 'success'] as const
5
+ export const COLOR_NAMES = [
6
+ 'primary',
7
+ 'error',
8
+ 'warning',
9
+ 'info',
10
+ 'success',
11
+ ] as const
6
12
  export type ColorName = (typeof COLOR_NAMES)[number]
7
13
 
8
14
  export const colorsSchema = z
9
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".
10
22
  light: rgbColorSchema.optional(),
11
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(),
12
31
  })
13
32
  .extend(
14
33
  Object.fromEntries(
@@ -0,0 +1,29 @@
1
+ import { Sub } from '../oidc/sub.js'
2
+ import { InvalidRequestError } from './invalid-request-error.js'
3
+
4
+ /**
5
+ * Thrown by {@link AccountStore.authenticateAccount} implementations to signal
6
+ * that a sign-in attempt was rejected because the provided credentials did not
7
+ * match a known account.
8
+ *
9
+ * Stores should populate {@link InvalidCredentialsError.sub} when the
10
+ * identifier resolved to an existing account but e.g. the password or OTP was
11
+ * incorrect. The identifier-unknown case should leave `sub` unset. This
12
+ * information is surfaced to the `onSignInFailed` hook and never sent back to
13
+ * the client, so populating it does not affect the client-visible response.
14
+ *
15
+ * Only the subject identifier (DID) is carried — not a full `Account` — to
16
+ * avoid embedding PII (email, name, etc.) in an error that may be serialized
17
+ * by loggers or monitoring tools walking the `.cause` chain. Hook consumers
18
+ * that need richer profile info can resolve it from their own account store
19
+ * using `sub`.
20
+ */
21
+ export class InvalidCredentialsError extends InvalidRequestError {
22
+ constructor(
23
+ message = 'Invalid identifier or password',
24
+ public readonly sub?: Sub,
25
+ cause?: unknown,
26
+ ) {
27
+ super(message, cause)
28
+ }
29
+ }
@@ -1,4 +1,8 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http'
1
+ import type {
2
+ IncomingHttpHeaders,
3
+ IncomingMessage,
4
+ ServerResponse,
5
+ } from 'node:http'
2
6
  import { SubCtx, subCtx } from './context.js'
3
7
  import { MethodMatcherInput } from './method.js'
4
8
  import { combineMiddlewares } from './middleware.js'
@@ -8,7 +12,7 @@ import { Middleware } from './types.js'
8
12
 
9
13
  export type RouterCtx<T extends object | void = void> = SubCtx<
10
14
  T,
11
- { url: Readonly<URL> }
15
+ { url: Readonly<URL>; headers: IncomingHttpHeaders }
12
16
  >
13
17
 
14
18
  export type RouterMiddleware<
@@ -93,7 +97,7 @@ export class Router<
93
97
 
94
98
  // Any error thrown here will be uncaught/unhandled (a middleware should
95
99
  // never throw)
96
- const context = subCtx(this, { url })
100
+ const context = subCtx(this, { url, headers: req.headers })
97
101
  middleware.call(context, req, res, next)
98
102
  }
99
103
  }
@@ -5,6 +5,8 @@ export type HslColor = { h: number; s: number; l: number }
5
5
  export type RgbaColor = { r: number; g: number; b: number; a: number }
6
6
  export type HslaColor = { h: number; s: number; l: number; a: number }
7
7
 
8
+ export type Color = RgbColor | HslColor | RgbaColor | HslaColor
9
+
8
10
  export function parseColor(color: string): RgbColor | RgbaColor {
9
11
  if (color.startsWith('#')) {
10
12
  return parseHexColor(color)
@@ -77,10 +79,44 @@ export function pickContrastColor(ref: RgbColor, a: RgbColor, b: RgbColor) {
77
79
  return computeContrastRatio(ref, a) > computeContrastRatio(ref, b) ? a : b
78
80
  }
79
81
 
82
+ export function hslToRgb({ h, s, l }: HslColor): RgbColor
83
+ export function hslToRgb({ h, s, l, a }: HslaColor): RgbaColor
84
+ export function hslToRgb(input: HslaColor | HslColor): RgbColor | RgbaColor {
85
+ const { h, s, l } = input
86
+
87
+ // Achromatic (gray)
88
+ if (s === 0) {
89
+ const gray = Math.round(l * 255)
90
+ return 'a' in input
91
+ ? { r: gray, g: gray, b: gray, a: input.a }
92
+ : { r: gray, g: gray, b: gray }
93
+ }
94
+
95
+ const hueToRgb = (p: number, q: number, t: number): number => {
96
+ if (t < 0) t += 1
97
+ if (t > 1) t -= 1
98
+ if (t < 1 / 6) return p + (q - p) * 6 * t
99
+ if (t < 1 / 2) return q
100
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
101
+ return p
102
+ }
103
+
104
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s
105
+ const p = 2 * l - q
106
+ const hNorm = h / 360
107
+
108
+ const r = Math.round(hueToRgb(p, q, hNorm + 1 / 3) * 255)
109
+ const g = Math.round(hueToRgb(p, q, hNorm) * 255)
110
+ const b = Math.round(hueToRgb(p, q, hNorm - 1 / 3) * 255)
111
+
112
+ return 'a' in input ? { r, g, b, a: input.a } : { r, g, b }
113
+ }
114
+
80
115
  /**
81
116
  * @see {@link https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef}
82
117
  */
83
- function relativeLuminance({ r, g, b }: RgbColor) {
118
+ function relativeLuminance(color: HslColor | RgbColor) {
119
+ const { r, g, b } = 'h' in color ? hslToRgb(color) : color
84
120
  return rgbLum(r) * 0.2126 + rgbLum(g) * 0.7152 + rgbLum(b) * 0.0722
85
121
  }
86
122
 
@@ -10,6 +10,7 @@ export * from './errors/invalid-authorization-details-error.js'
10
10
  export * from './errors/invalid-client-error.js'
11
11
  export * from './errors/invalid-client-id-error.js'
12
12
  export * from './errors/invalid-client-metadata-error.js'
13
+ export * from './errors/invalid-credentials-error.js'
13
14
  export * from './errors/invalid-dpop-key-binding-error.js'
14
15
  export * from './errors/invalid-dpop-proof-error.js'
15
16
  export * from './errors/invalid-grant-error.js'
@@ -23,6 +23,7 @@ import { DeviceId } from './device/device-id.js'
23
23
  import { DpopProof } from './dpop/dpop-proof.js'
24
24
  import { AccessDeniedError } from './errors/access-denied-error.js'
25
25
  import { AuthorizationError } from './errors/authorization-error.js'
26
+ import { InvalidCredentialsError } from './errors/invalid-credentials-error.js'
26
27
  import { InvalidRequestError } from './errors/invalid-request-error.js'
27
28
  import { OAuthError } from './errors/oauth-error.js'
28
29
  import {
@@ -32,6 +33,7 @@ import {
32
33
  } from './lib/hcaptcha.js'
33
34
  import { RequestMetadata } from './lib/http/request.js'
34
35
  import { Awaitable, OmitKey } from './lib/util/type.js'
36
+ import { Sub } from './oidc/sub.js'
35
37
  import { RequestId } from './request/request-id.js'
36
38
  import { AccessTokenPayload } from './signer/access-token-payload.js'
37
39
  import { TokenClaims } from './token/token-claims.js'
@@ -52,6 +54,7 @@ export {
52
54
  type HcaptchaClientTokens,
53
55
  type HcaptchaConfig,
54
56
  type HcaptchaVerifyResult,
57
+ InvalidCredentialsError,
55
58
  InvalidRequestError,
56
59
  type Jwks,
57
60
  type OAuthAccessToken,
@@ -67,6 +70,7 @@ export {
67
70
  type SignInData,
68
71
  type SignUpData,
69
72
  type SignUpInput,
73
+ type Sub,
70
74
  type TokenClaims,
71
75
  }
72
76
 
@@ -159,15 +163,25 @@ export type OAuthHooks = {
159
163
  deviceMetadata: RequestMetadata
160
164
  }) => Awaitable<void>
161
165
 
166
+ /**
167
+ * `clientId` is populated when the sign-in is submitted in the context of
168
+ * an OAuth authorization request (i.e. the user is logging in to approve a
169
+ * client); it is omitted for first-party sign-ins that happen outside any
170
+ * authorization flow.
171
+ */
162
172
  onSignInAttempt?: (data: {
163
173
  data: SignInData
164
174
  deviceId: DeviceId
165
175
  deviceMetadata: RequestMetadata
176
+ clientId?: ClientId
166
177
  }) => Awaitable<void>
167
178
 
168
179
  /**
169
180
  * This hook is called when a user successfully signs in.
170
181
  *
182
+ * `clientId` is populated when the sign-in is submitted in the context of
183
+ * an OAuth authorization request; see {@link OAuthHooks.onSignInAttempt}.
184
+ *
171
185
  * @throws {InvalidRequestError} when the sing-in should be denied
172
186
  */
173
187
  onSignedIn?: (data: {
@@ -175,6 +189,34 @@ export type OAuthHooks = {
175
189
  account: Account
176
190
  deviceId: DeviceId
177
191
  deviceMetadata: RequestMetadata
192
+ clientId?: ClientId
193
+ }) => Awaitable<void>
194
+
195
+ /**
196
+ * This hook is called when a sign-in attempt is rejected by the account
197
+ * store due to invalid credentials (e.g. unknown identifier, wrong
198
+ * password). It is *not* called for unexpected server errors, nor for flows
199
+ * that require an additional authentication factor.
200
+ *
201
+ * `sub` is populated when the store throws an
202
+ * {@link InvalidCredentialsError} that carries the matched subject
203
+ * identifier (i.e. identifier known, credentials wrong). It is `null` when
204
+ * the identifier was unknown or when the store threw a plain
205
+ * {@link InvalidRequestError} without distinguishing the two cases.
206
+ *
207
+ * `clientId` is populated when the sign-in is submitted in the context of
208
+ * an OAuth authorization request; see {@link OAuthHooks.onSignInAttempt}.
209
+ *
210
+ * Errors thrown from this hook are caught and ignored so that they do not
211
+ * mask the original authentication failure.
212
+ */
213
+ onSignInFailed?: (data: {
214
+ data: SignInData
215
+ error: InvalidRequestError
216
+ sub: Sub | null
217
+ deviceId: DeviceId
218
+ deviceMetadata: RequestMetadata
219
+ clientId?: ClientId
178
220
  }) => Awaitable<void>
179
221
 
180
222
  /**
@@ -704,19 +704,13 @@ export class OAuthProvider extends OAuthVerifier {
704
704
  client,
705
705
  parameters,
706
706
  requestUri,
707
- sessions: sessions.map((session) => ({
708
- // Map to avoid leaking other data that might be present in the session
709
- account: session.account,
710
- loginRequired: session.loginRequired,
711
- consentRequired: session.consentRequired,
712
-
713
- selected:
714
- parameters.prompt == null ||
715
- parameters.prompt === 'login' ||
716
- parameters.prompt === 'consent'
717
- ? matchesHint.call(parameters, session)
718
- : false,
719
- })),
707
+ sessions,
708
+ selectedSub:
709
+ parameters.prompt == null ||
710
+ parameters.prompt === 'login' ||
711
+ parameters.prompt === 'consent'
712
+ ? sessions.find(matchesHint, parameters)?.account.sub
713
+ : undefined,
720
714
  permissionSets: await this.lexiconManager
721
715
  .getPermissionSetsFromScope(parameters.scope)
722
716
  .catch((cause) => {
@@ -316,6 +316,18 @@ export class RequestManager {
316
316
  return parameters
317
317
  }
318
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
+
319
331
  async get(requestUri: RequestUri, deviceId?: DeviceId, clientId?: ClientId) {
320
332
  const requestId = decodeRequestUri(requestUri)
321
333
 
@@ -1,5 +1,5 @@
1
1
  import type { LexiconPermissionSet } from '@atproto/lex-document'
2
- import type { Session } from '@atproto/oauth-provider-api'
2
+ import type { Account, Session } from '@atproto/oauth-provider-api'
3
3
  import type { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'
4
4
  import type { Client } from '../client/client.js'
5
5
  import type { RequestUri } from '../request/request-uri.js'
@@ -12,4 +12,5 @@ export type AuthorizationResultAuthorizePage = {
12
12
 
13
13
  requestUri: RequestUri
14
14
  sessions: readonly Session[]
15
+ selectedSub?: Account['sub']
15
16
  }
@@ -1,5 +1,4 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http'
2
- import type { HydrationData as FeHydrationData } from '@atproto/oauth-provider-frontend/hydration-data'
3
2
  import type { HydrationData as UiHydrationData } from '@atproto/oauth-provider-ui/hydration-data'
4
3
  import { buildCustomizationCss } from '../../customization/build-customization-css.js'
5
4
  import { buildCustomizationData } from '../../customization/build-customization-data.js'
@@ -7,7 +6,6 @@ import { Customization } from '../../customization/customization.js'
7
6
  import { CspConfig, mergeCsp } from '../../lib/csp/index.js'
8
7
  import { declareHydrationData } from '../../lib/html/hydration-data.js'
9
8
  import { cssCode, html } from '../../lib/html/index.js'
10
- import { combineMiddlewares } from '../../lib/http/middleware.js'
11
9
  import { WriteResponseOptions } from '../../lib/http/response.js'
12
10
  import {
13
11
  CrossOriginEmbedderPolicy,
@@ -29,24 +27,18 @@ import { setupCsrfToken } from './csrf.js'
29
27
  const ui = parseAssetsManifest(
30
28
  require.resolve('@atproto/oauth-provider-ui/bundle-manifest.json'),
31
29
  )
32
- const fe = parseAssetsManifest(
33
- require.resolve('@atproto/oauth-provider-frontend/bundle-manifest.json'),
34
- )
35
30
 
36
- type HydrationData = Simplify<UiHydrationData & FeHydrationData>
31
+ type HydrationData = Simplify<UiHydrationData>
37
32
 
38
33
  function getAssets(entryName: keyof HydrationData) {
39
- const assetRef = ui.getAssets(entryName) || fe.getAssets(entryName)
34
+ const assetRef = ui.getAssets(entryName)
40
35
  if (assetRef) return assetRef
41
36
 
42
37
  // Fool-proof. Should never happen.
43
38
  throw new Error(`Entry "${entryName}" not found in assets`)
44
39
  }
45
40
 
46
- export const assetsMiddleware = combineMiddlewares([
47
- ui.assetsMiddleware,
48
- fe.assetsMiddleware,
49
- ])
41
+ export const assetsMiddleware = ui.assetsMiddleware
50
42
 
51
43
  const SPA_CSP: CspConfig = {
52
44
  // API calls are made to the same origin
@@ -81,9 +73,25 @@ export function sendWebAppFactory<P extends keyof HydrationData>(
81
73
 
82
74
  const csp = mergeCsp(
83
75
  SPA_CSP,
84
- customization?.hcaptcha ? HCAPTCHA_CSP : undefined,
76
+ customization.hcaptcha ? HCAPTCHA_CSP : undefined,
85
77
  )
86
78
 
79
+ const coep = customization.hcaptcha
80
+ ? // hCaptcha's implementation of COEP is currently broken. Let's disable it
81
+ // to avoid breaking the entire page.
82
+ //
83
+ // https://github.com/hCaptcha/react-hcaptcha/issues/259
84
+ // https://github.com/hCaptcha/react-hcaptcha/issues/380
85
+ CrossOriginEmbedderPolicy.unsafeNone
86
+ : // Since we are loading avatars form other origins, which might not have
87
+ // CORP headers, we need to use the "credentialless" value, which allows
88
+ // loading cross-origin resources without credentials (cookies, client
89
+ // certificates, etc.). This is a more secure alternative to
90
+ // "unsafe-none". Ideally, we would want to set COEP to "require-corp" and
91
+ // ensure that all cross-origin resources have the appropriate CORP
92
+ // headers.
93
+ CrossOriginEmbedderPolicy.credentialless
94
+
87
95
  return async function sendWebApp(
88
96
  req: IncomingMessage,
89
97
  res: ServerResponse,
@@ -101,12 +109,9 @@ export function sendWebAppFactory<P extends keyof HydrationData>(
101
109
  return writeHtml(
102
110
  res,
103
111
  mergeDefaults<WriteHtmlOptions>(defaults, options, {
104
- bodyAttrs: {
105
- class:
106
- 'bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100',
107
- },
112
+ bodyAttrs: { class: 'text-text-default bg-contrast-0' },
108
113
  csp: options?.csp ? mergeCsp(csp, options.csp) : csp,
109
- coep: options?.coep ?? CrossOriginEmbedderPolicy.credentialless,
114
+ coep: options?.coep ?? coep,
110
115
  meta: [{ name: 'robots', content: 'noindex' }],
111
116
  body: html`<div id="root"></div>`,
112
117
  scripts: [script, ...scripts],
@@ -33,6 +33,7 @@ export function sendAuthorizePageFactory(
33
33
  loginHint: data.parameters.login_hint,
34
34
  promptMode: data.parameters.prompt,
35
35
  permissionSets: Object.fromEntries(data.permissionSets),
36
+ selectedSub: data.selectedSub,
36
37
  },
37
38
  __sessions: data.sessions,
38
39
  },
@@ -145,10 +145,17 @@ export function createApiMiddleware<
145
145
  // Remember when not in the context of a request by default
146
146
  const { remember = requestUri == null, ...input } = this.input
147
147
 
148
+ // Look up the client identifier associated with the pending OAuth
149
+ // request, if any, so it can be surfaced to the sign-in hooks.
150
+ const clientId = requestUri
151
+ ? await server.requestManager.peekClientId(requestUri)
152
+ : undefined
153
+
148
154
  const account = await server.accountManager.authenticateAccount(
149
155
  deviceId,
150
156
  deviceMetadata,
151
157
  input,
158
+ clientId,
152
159
  )
153
160
 
154
161
  if (remember) {
@@ -576,14 +583,14 @@ export function createApiMiddleware<
576
583
  async function authenticate(
577
584
  this: ApiContext<void, { sub: Sub }>,
578
585
  req: Req,
579
- res: Res,
586
+ _res: Res,
580
587
  ) {
581
- const authorization = req.headers.authorization?.split(' ')
582
- if (authorization?.[0].toLowerCase() === 'bearer') {
588
+ if (req.headers.authorization?.startsWith('Bearer ')) {
583
589
  try {
584
590
  // If there is an authorization header, verify that the ephemeral token it
585
591
  // contains is a jwt bound to the right [sub, device, request].
586
- const ephemeralToken = signedJwtSchema.parse(authorization[1])
592
+ const bearer = req.headers.authorization.slice(7)
593
+ const ephemeralToken = signedJwtSchema.parse(bearer)
587
594
  const { payload } =
588
595
  await server.signer.verifyEphemeralToken(ephemeralToken)
589
596
 
@@ -595,8 +602,12 @@ export function createApiMiddleware<
595
602
  return await server.accountManager.getAccount(payload.sub)
596
603
  }
597
604
  } catch (err) {
598
- onError?.(req, res, err, 'Failed to authenticate ephemeral token')
599
- // Fall back to session based authentication
605
+ throw new WWWAuthenticateError(
606
+ 'unauthorized',
607
+ `Invalid or expired bearer token`,
608
+ { Bearer: {} },
609
+ err,
610
+ )
600
611
  }
601
612
  }
602
613
 
@@ -1 +1 @@
1
- {"root":["./src/constants.ts","./src/index.ts","./src/oauth-client.ts","./src/oauth-dpop.ts","./src/oauth-errors.ts","./src/oauth-hooks.ts","./src/oauth-middleware.ts","./src/oauth-provider.ts","./src/oauth-store.ts","./src/oauth-verifier.ts","./src/access-token/access-token-mode.ts","./src/account/account-manager.ts","./src/account/account-store.ts","./src/account/sign-in-data.ts","./src/account/sign-up-input.ts","./src/client/client-auth.ts","./src/client/client-data.ts","./src/client/client-id.ts","./src/client/client-info.ts","./src/client/client-manager.ts","./src/client/client-store.ts","./src/client/client-utils.ts","./src/client/client.ts","./src/customization/branding.ts","./src/customization/build-customization-css.ts","./src/customization/build-customization-data.ts","./src/customization/colors.ts","./src/customization/customization.ts","./src/customization/links.ts","./src/device/device-data.ts","./src/device/device-id.ts","./src/device/device-manager.ts","./src/device/device-store.ts","./src/device/session-id.ts","./src/dpop/dpop-manager.ts","./src/dpop/dpop-nonce.ts","./src/dpop/dpop-proof.ts","./src/errors/access-denied-error.ts","./src/errors/account-selection-required-error.ts","./src/errors/authorization-error.ts","./src/errors/consent-required-error.ts","./src/errors/error-parser.ts","./src/errors/handle-unavailable-error.ts","./src/errors/invalid-authorization-details-error.ts","./src/errors/invalid-client-error.ts","./src/errors/invalid-client-id-error.ts","./src/errors/invalid-client-metadata-error.ts","./src/errors/invalid-dpop-key-binding-error.ts","./src/errors/invalid-dpop-proof-error.ts","./src/errors/invalid-grant-error.ts","./src/errors/invalid-invite-code-error.ts","./src/errors/invalid-redirect-uri-error.ts","./src/errors/invalid-request-error.ts","./src/errors/invalid-scope-error.ts","./src/errors/invalid-token-error.ts","./src/errors/login-required-error.ts","./src/errors/oauth-error.ts","./src/errors/second-authentication-factor-required-error.ts","./src/errors/unauthorized-client-error.ts","./src/errors/use-dpop-nonce-error.ts","./src/errors/www-authenticate-error.ts","./src/lexicon/lexicon-data.ts","./src/lexicon/lexicon-getter.ts","./src/lexicon/lexicon-manager.ts","./src/lexicon/lexicon-store.ts","./src/lib/hcaptcha.ts","./src/lib/nsid.ts","./src/lib/redis.ts","./src/lib/write-form-redirect.ts","./src/lib/write-html.ts","./src/lib/csp/index.ts","./src/lib/html/build-document.ts","./src/lib/html/escapers.ts","./src/lib/html/html.ts","./src/lib/html/hydration-data.ts","./src/lib/html/index.ts","./src/lib/html/tags.ts","./src/lib/html/util.ts","./src/lib/http/accept.ts","./src/lib/http/context.ts","./src/lib/http/headers.ts","./src/lib/http/index.ts","./src/lib/http/method.ts","./src/lib/http/middleware.ts","./src/lib/http/parser.ts","./src/lib/http/path.ts","./src/lib/http/request.ts","./src/lib/http/response.ts","./src/lib/http/route.ts","./src/lib/http/router.ts","./src/lib/http/security-headers.ts","./src/lib/http/stream.ts","./src/lib/http/types.ts","./src/lib/http/url.ts","./src/lib/util/authorization-header.ts","./src/lib/util/cast.ts","./src/lib/util/color.ts","./src/lib/util/crypto.ts","./src/lib/util/date.ts","./src/lib/util/error.ts","./src/lib/util/function.ts","./src/lib/util/locale.ts","./src/lib/util/object.ts","./src/lib/util/redirect-uri.ts","./src/lib/util/time.ts","./src/lib/util/type.ts","./src/lib/util/ui8.ts","./src/lib/util/well-known.ts","./src/lib/util/zod-error.ts","./src/metadata/build-metadata.ts","./src/oidc/sub.ts","./src/replay/replay-manager.ts","./src/replay/replay-store-memory.ts","./src/replay/replay-store-redis.ts","./src/replay/replay-store.ts","./src/request/code.ts","./src/request/request-data.ts","./src/request/request-id.ts","./src/request/request-manager.ts","./src/request/request-store.ts","./src/request/request-uri.ts","./src/result/authorization-redirect-parameters.ts","./src/result/authorization-result-authorize-page.ts","./src/result/authorization-result-redirect.ts","./src/router/create-account-page-middleware.ts","./src/router/create-api-middleware.ts","./src/router/create-authorization-page-middleware.ts","./src/router/create-oauth-middleware.ts","./src/router/error-handler.ts","./src/router/middleware-options.ts","./src/router/assets/assets-manifest.ts","./src/router/assets/assets.ts","./src/router/assets/csrf.ts","./src/router/assets/send-account-page.ts","./src/router/assets/send-authorization-page.ts","./src/router/assets/send-cookie-error-page.ts","./src/router/assets/send-error-page.ts","./src/router/assets/send-redirect.ts","./src/signer/access-token-payload.ts","./src/signer/api-token-payload.ts","./src/signer/signer.ts","./src/token/refresh-token.ts","./src/token/token-claims.ts","./src/token/token-data.ts","./src/token/token-id.ts","./src/token/token-manager.ts","./src/token/token-store.ts","./src/types/authorization-response-error.ts","./src/types/color-hue.ts","./src/types/email-otp.ts","./src/types/email.ts","./src/types/handle.ts","./src/types/invite-code.ts","./src/types/par-response-error.ts","./src/types/password.ts","./src/types/rgb-color.ts"],"version":"5.8.3"}
1
+ {"root":["./src/constants.ts","./src/index.ts","./src/oauth-client.ts","./src/oauth-dpop.ts","./src/oauth-errors.ts","./src/oauth-hooks.ts","./src/oauth-middleware.ts","./src/oauth-provider.ts","./src/oauth-store.ts","./src/oauth-verifier.ts","./src/access-token/access-token-mode.ts","./src/account/account-manager.ts","./src/account/account-store.ts","./src/account/sign-in-data.ts","./src/account/sign-up-input.ts","./src/client/client-auth.ts","./src/client/client-data.ts","./src/client/client-id.ts","./src/client/client-info.ts","./src/client/client-manager.ts","./src/client/client-store.ts","./src/client/client-utils.ts","./src/client/client.ts","./src/customization/branding.ts","./src/customization/build-customization-css.ts","./src/customization/build-customization-data.ts","./src/customization/colors.ts","./src/customization/customization.ts","./src/customization/links.ts","./src/device/device-data.ts","./src/device/device-id.ts","./src/device/device-manager.ts","./src/device/device-store.ts","./src/device/session-id.ts","./src/dpop/dpop-manager.ts","./src/dpop/dpop-nonce.ts","./src/dpop/dpop-proof.ts","./src/errors/access-denied-error.ts","./src/errors/account-selection-required-error.ts","./src/errors/authorization-error.ts","./src/errors/consent-required-error.ts","./src/errors/error-parser.ts","./src/errors/handle-unavailable-error.ts","./src/errors/invalid-authorization-details-error.ts","./src/errors/invalid-client-error.ts","./src/errors/invalid-client-id-error.ts","./src/errors/invalid-client-metadata-error.ts","./src/errors/invalid-credentials-error.ts","./src/errors/invalid-dpop-key-binding-error.ts","./src/errors/invalid-dpop-proof-error.ts","./src/errors/invalid-grant-error.ts","./src/errors/invalid-invite-code-error.ts","./src/errors/invalid-redirect-uri-error.ts","./src/errors/invalid-request-error.ts","./src/errors/invalid-scope-error.ts","./src/errors/invalid-token-error.ts","./src/errors/login-required-error.ts","./src/errors/oauth-error.ts","./src/errors/second-authentication-factor-required-error.ts","./src/errors/unauthorized-client-error.ts","./src/errors/use-dpop-nonce-error.ts","./src/errors/www-authenticate-error.ts","./src/lexicon/lexicon-data.ts","./src/lexicon/lexicon-getter.ts","./src/lexicon/lexicon-manager.ts","./src/lexicon/lexicon-store.ts","./src/lib/hcaptcha.ts","./src/lib/nsid.ts","./src/lib/redis.ts","./src/lib/write-form-redirect.ts","./src/lib/write-html.ts","./src/lib/csp/index.ts","./src/lib/html/build-document.ts","./src/lib/html/escapers.ts","./src/lib/html/html.ts","./src/lib/html/hydration-data.ts","./src/lib/html/index.ts","./src/lib/html/tags.ts","./src/lib/html/util.ts","./src/lib/http/accept.ts","./src/lib/http/context.ts","./src/lib/http/headers.ts","./src/lib/http/index.ts","./src/lib/http/method.ts","./src/lib/http/middleware.ts","./src/lib/http/parser.ts","./src/lib/http/path.ts","./src/lib/http/request.ts","./src/lib/http/response.ts","./src/lib/http/route.ts","./src/lib/http/router.ts","./src/lib/http/security-headers.ts","./src/lib/http/stream.ts","./src/lib/http/types.ts","./src/lib/http/url.ts","./src/lib/util/authorization-header.ts","./src/lib/util/cast.ts","./src/lib/util/color.ts","./src/lib/util/crypto.ts","./src/lib/util/date.ts","./src/lib/util/error.ts","./src/lib/util/function.ts","./src/lib/util/locale.ts","./src/lib/util/object.ts","./src/lib/util/redirect-uri.ts","./src/lib/util/time.ts","./src/lib/util/type.ts","./src/lib/util/ui8.ts","./src/lib/util/well-known.ts","./src/lib/util/zod-error.ts","./src/metadata/build-metadata.ts","./src/oidc/sub.ts","./src/replay/replay-manager.ts","./src/replay/replay-store-memory.ts","./src/replay/replay-store-redis.ts","./src/replay/replay-store.ts","./src/request/code.ts","./src/request/request-data.ts","./src/request/request-id.ts","./src/request/request-manager.ts","./src/request/request-store.ts","./src/request/request-uri.ts","./src/result/authorization-redirect-parameters.ts","./src/result/authorization-result-authorize-page.ts","./src/result/authorization-result-redirect.ts","./src/router/create-account-page-middleware.ts","./src/router/create-api-middleware.ts","./src/router/create-authorization-page-middleware.ts","./src/router/create-oauth-middleware.ts","./src/router/error-handler.ts","./src/router/middleware-options.ts","./src/router/assets/assets-manifest.ts","./src/router/assets/assets.ts","./src/router/assets/csrf.ts","./src/router/assets/send-account-page.ts","./src/router/assets/send-authorization-page.ts","./src/router/assets/send-cookie-error-page.ts","./src/router/assets/send-error-page.ts","./src/router/assets/send-redirect.ts","./src/signer/access-token-payload.ts","./src/signer/api-token-payload.ts","./src/signer/signer.ts","./src/token/refresh-token.ts","./src/token/token-claims.ts","./src/token/token-data.ts","./src/token/token-id.ts","./src/token/token-manager.ts","./src/token/token-store.ts","./src/types/authorization-response-error.ts","./src/types/color-hue.ts","./src/types/email-otp.ts","./src/types/email.ts","./src/types/handle.ts","./src/types/invite-code.ts","./src/types/par-response-error.ts","./src/types/password.ts","./src/types/rgb-color.ts"],"version":"5.8.3"}