@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.
- package/CHANGELOG.md +25 -0
- package/dist/account/account-manager.d.ts +2 -1
- package/dist/account/account-manager.d.ts.map +1 -1
- package/dist/account/account-manager.js +45 -4
- package/dist/account/account-manager.js.map +1 -1
- package/dist/account/account-store.d.ts +10 -4
- package/dist/account/account-store.d.ts.map +1 -1
- package/dist/account/account-store.js +2 -1
- package/dist/account/account-store.js.map +1 -1
- package/dist/customization/branding.d.ts +18 -1
- package/dist/customization/branding.d.ts.map +1 -1
- package/dist/customization/build-customization-css.d.ts.map +1 -1
- package/dist/customization/build-customization-css.js +16 -2
- package/dist/customization/build-customization-css.js.map +1 -1
- package/dist/customization/build-customization-data.d.ts +2 -2
- package/dist/customization/build-customization-data.d.ts.map +1 -1
- package/dist/customization/build-customization-data.js.map +1 -1
- package/dist/customization/colors.d.ts +11 -2
- package/dist/customization/colors.d.ts.map +1 -1
- package/dist/customization/colors.js +19 -1
- package/dist/customization/colors.js.map +1 -1
- package/dist/customization/customization.d.ts +26 -1
- package/dist/customization/customization.d.ts.map +1 -1
- package/dist/errors/invalid-credentials-error.d.ts +24 -0
- package/dist/errors/invalid-credentials-error.d.ts.map +1 -0
- package/dist/errors/invalid-credentials-error.js +30 -0
- package/dist/errors/invalid-credentials-error.js.map +1 -0
- package/dist/lib/http/router.d.ts +2 -1
- package/dist/lib/http/router.d.ts.map +1 -1
- package/dist/lib/http/router.js +1 -1
- package/dist/lib/http/router.js.map +1 -1
- package/dist/lib/util/color.d.ts +3 -0
- package/dist/lib/util/color.d.ts.map +1 -1
- package/dist/lib/util/color.js +33 -1
- package/dist/lib/util/color.js.map +1 -1
- package/dist/oauth-errors.d.ts +1 -0
- package/dist/oauth-errors.d.ts.map +1 -1
- package/dist/oauth-errors.js +1 -0
- package/dist/oauth-errors.js.map +1 -1
- package/dist/oauth-hooks.d.ts +40 -1
- package/dist/oauth-hooks.d.ts.map +1 -1
- package/dist/oauth-hooks.js +3 -1
- package/dist/oauth-hooks.js.map +1 -1
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +6 -11
- package/dist/oauth-provider.js.map +1 -1
- package/dist/request/request-manager.d.ts +7 -0
- package/dist/request/request-manager.d.ts.map +1 -1
- package/dist/request/request-manager.js +11 -0
- package/dist/request/request-manager.js.map +1 -1
- package/dist/result/authorization-result-authorize-page.d.ts +2 -1
- package/dist/result/authorization-result-authorize-page.d.ts.map +1 -1
- package/dist/result/authorization-result-authorize-page.js.map +1 -1
- package/dist/router/assets/assets.d.ts +1 -2
- package/dist/router/assets/assets.d.ts.map +1 -1
- package/dist/router/assets/assets.js +20 -12
- package/dist/router/assets/assets.js.map +1 -1
- package/dist/router/assets/send-authorization-page.d.ts.map +1 -1
- package/dist/router/assets/send-authorization-page.js +1 -0
- package/dist/router/assets/send-authorization-page.js.map +1 -1
- package/dist/router/create-api-middleware.d.ts.map +1 -1
- package/dist/router/create-api-middleware.js +11 -7
- package/dist/router/create-api-middleware.js.map +1 -1
- package/package.json +10 -11
- package/src/account/account-manager.ts +49 -6
- package/src/account/account-store.ts +10 -2
- package/src/customization/build-customization-css.ts +25 -3
- package/src/customization/build-customization-data.ts +2 -2
- package/src/customization/colors.ts +20 -1
- package/src/errors/invalid-credentials-error.ts +29 -0
- package/src/lib/http/router.ts +7 -3
- package/src/lib/util/color.ts +37 -1
- package/src/oauth-errors.ts +1 -0
- package/src/oauth-hooks.ts +42 -0
- package/src/oauth-provider.ts +7 -13
- package/src/request/request-manager.ts +12 -0
- package/src/result/authorization-result-authorize-page.ts +2 -1
- package/src/router/assets/assets.ts +22 -17
- package/src/router/assets/send-authorization-page.ts +1 -0
- package/src/router/create-api-middleware.ts +17 -6
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
|
16
|
-
|
|
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 = [
|
|
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
|
+
}
|
package/src/lib/http/router.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
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
|
}
|
package/src/lib/util/color.ts
CHANGED
|
@@ -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(
|
|
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
|
|
package/src/oauth-errors.ts
CHANGED
|
@@ -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'
|
package/src/oauth-hooks.ts
CHANGED
|
@@ -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
|
/**
|
package/src/oauth-provider.ts
CHANGED
|
@@ -704,19 +704,13 @@ export class OAuthProvider extends OAuthVerifier {
|
|
|
704
704
|
client,
|
|
705
705
|
parameters,
|
|
706
706
|
requestUri,
|
|
707
|
-
sessions
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
|
31
|
+
type HydrationData = Simplify<UiHydrationData>
|
|
37
32
|
|
|
38
33
|
function getAssets(entryName: keyof HydrationData) {
|
|
39
|
-
const assetRef = ui.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 =
|
|
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
|
|
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 ??
|
|
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],
|
|
@@ -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
|
-
|
|
586
|
+
_res: Res,
|
|
580
587
|
) {
|
|
581
|
-
|
|
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
|
|
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
|
-
|
|
599
|
-
|
|
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"}
|