@atproto/oauth-provider 0.19.7 → 0.19.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/errors/error-parser.js +5 -5
- package/dist/errors/error-parser.js.map +1 -1
- package/package.json +22 -18
- package/src/access-token/access-token-mode.ts +0 -4
- package/src/account/account-manager.ts +0 -591
- package/src/account/account-store.ts +0 -305
- package/src/account/sign-in-data.ts +0 -15
- package/src/account/sign-up-input.ts +0 -20
- package/src/client/client-auth.ts +0 -64
- package/src/client/client-data.ts +0 -9
- package/src/client/client-id.ts +0 -4
- package/src/client/client-info.ts +0 -13
- package/src/client/client-manager.ts +0 -772
- package/src/client/client-store.ts +0 -35
- package/src/client/client-utils.ts +0 -37
- package/src/client/client.ts +0 -394
- package/src/constants.ts +0 -80
- package/src/customization/branding.ts +0 -12
- package/src/customization/build-customization-css.ts +0 -55
- package/src/customization/build-customization-data.ts +0 -24
- package/src/customization/colors.ts +0 -48
- package/src/customization/customization.ts +0 -29
- package/src/customization/links.ts +0 -10
- package/src/device/device-data.ts +0 -11
- package/src/device/device-id.ts +0 -27
- package/src/device/device-manager.ts +0 -292
- package/src/device/device-store.ts +0 -31
- package/src/device/session-id.ts +0 -21
- package/src/dpop/dpop-manager.ts +0 -214
- package/src/dpop/dpop-nonce.ts +0 -121
- package/src/dpop/dpop-proof.ts +0 -6
- package/src/errors/access-denied-error.ts +0 -12
- package/src/errors/account-selection-required-error.ts +0 -12
- package/src/errors/authorization-error.ts +0 -45
- package/src/errors/consent-required-error.ts +0 -12
- package/src/errors/error-parser.ts +0 -147
- package/src/errors/handle-unavailable-error.ts +0 -23
- package/src/errors/invalid-authorization-details-error.ts +0 -27
- package/src/errors/invalid-client-error.ts +0 -20
- package/src/errors/invalid-client-id-error.ts +0 -31
- package/src/errors/invalid-client-metadata-error.ts +0 -68
- package/src/errors/invalid-credentials-error.ts +0 -29
- package/src/errors/invalid-dpop-key-binding-error.ts +0 -21
- package/src/errors/invalid-dpop-proof-error.ts +0 -13
- package/src/errors/invalid-grant-error.ts +0 -21
- package/src/errors/invalid-invite-code-error.ts +0 -10
- package/src/errors/invalid-redirect-uri-error.ts +0 -17
- package/src/errors/invalid-request-error.ts +0 -32
- package/src/errors/invalid-scope-error.ts +0 -15
- package/src/errors/invalid-token-error.ts +0 -60
- package/src/errors/login-required-error.ts +0 -12
- package/src/errors/oauth-error.ts +0 -28
- package/src/errors/second-authentication-factor-required-error.ts +0 -25
- package/src/errors/unauthorized-client-error.ts +0 -20
- package/src/errors/use-dpop-nonce-error.ts +0 -32
- package/src/errors/www-authenticate-error.ts +0 -64
- package/src/index.ts +0 -16
- package/src/lexicon/lexicon-data.ts +0 -11
- package/src/lexicon/lexicon-getter.ts +0 -55
- package/src/lexicon/lexicon-manager.ts +0 -116
- package/src/lexicon/lexicon-store.ts +0 -35
- package/src/lib/csp/index.ts +0 -101
- package/src/lib/hcaptcha.ts +0 -228
- package/src/lib/html/README.md +0 -9
- package/src/lib/html/build-document.ts +0 -151
- package/src/lib/html/escapers.ts +0 -66
- package/src/lib/html/html.ts +0 -56
- package/src/lib/html/hydration-data.ts +0 -19
- package/src/lib/html/index.ts +0 -5
- package/src/lib/html/tags.ts +0 -67
- package/src/lib/html/util.ts +0 -17
- package/src/lib/http/README.md +0 -11
- package/src/lib/http/accept.ts +0 -90
- package/src/lib/http/context.ts +0 -42
- package/src/lib/http/headers.ts +0 -15
- package/src/lib/http/index.ts +0 -10
- package/src/lib/http/method.ts +0 -18
- package/src/lib/http/middleware.ts +0 -169
- package/src/lib/http/parser.ts +0 -101
- package/src/lib/http/path.ts +0 -82
- package/src/lib/http/request.ts +0 -256
- package/src/lib/http/response.ts +0 -122
- package/src/lib/http/route.ts +0 -60
- package/src/lib/http/router.ts +0 -117
- package/src/lib/http/security-headers.ts +0 -100
- package/src/lib/http/stream.ts +0 -57
- package/src/lib/http/types.ts +0 -16
- package/src/lib/http/url.ts +0 -23
- package/src/lib/nsid.ts +0 -10
- package/src/lib/redis.ts +0 -25
- package/src/lib/util/authorization-header.ts +0 -28
- package/src/lib/util/cast.ts +0 -18
- package/src/lib/util/color.ts +0 -168
- package/src/lib/util/crypto.ts +0 -32
- package/src/lib/util/date.ts +0 -7
- package/src/lib/util/error.ts +0 -7
- package/src/lib/util/function.ts +0 -39
- package/src/lib/util/locale.ts +0 -12
- package/src/lib/util/object.ts +0 -23
- package/src/lib/util/redirect-uri.ts +0 -46
- package/src/lib/util/time.ts +0 -49
- package/src/lib/util/type.ts +0 -182
- package/src/lib/util/ui8.ts +0 -14
- package/src/lib/util/well-known.ts +0 -8
- package/src/lib/util/zod-error.ts +0 -26
- package/src/lib/write-form-redirect.ts +0 -58
- package/src/lib/write-html.ts +0 -70
- package/src/metadata/build-metadata.ts +0 -141
- package/src/oauth-client.ts +0 -3
- package/src/oauth-dpop.ts +0 -2
- package/src/oauth-errors.ts +0 -26
- package/src/oauth-hooks.ts +0 -519
- package/src/oauth-middleware.ts +0 -53
- package/src/oauth-provider.ts +0 -1110
- package/src/oauth-store.ts +0 -12
- package/src/oauth-verifier.ts +0 -260
- package/src/replay/replay-manager.ts +0 -51
- package/src/replay/replay-store-memory.ts +0 -36
- package/src/replay/replay-store-redis.ts +0 -30
- package/src/replay/replay-store.ts +0 -44
- package/src/request/code.ts +0 -23
- package/src/request/request-data.ts +0 -36
- package/src/request/request-id.ts +0 -22
- package/src/request/request-manager.ts +0 -521
- package/src/request/request-store.ts +0 -60
- package/src/request/request-uri.ts +0 -42
- package/src/result/authorization-redirect-parameters.ts +0 -24
- package/src/result/authorization-result-authorize-page.ts +0 -16
- package/src/result/authorization-result-redirect.ts +0 -8
- package/src/router/assets/assets-manifest.ts +0 -117
- package/src/router/assets/assets.ts +0 -124
- package/src/router/assets/csrf.ts +0 -79
- package/src/router/assets/send-account-page.ts +0 -23
- package/src/router/assets/send-authorization-page.ts +0 -42
- package/src/router/assets/send-cookie-error-page.ts +0 -21
- package/src/router/assets/send-error-page.ts +0 -25
- package/src/router/assets/send-redirect.ts +0 -140
- package/src/router/create-account-page-middleware.ts +0 -88
- package/src/router/create-api-middleware.ts +0 -1051
- package/src/router/create-authorization-page-middleware.ts +0 -281
- package/src/router/create-oauth-middleware.ts +0 -257
- package/src/router/error-handler.ts +0 -6
- package/src/router/middleware-options.ts +0 -9
- package/src/signer/access-token-payload.ts +0 -25
- package/src/signer/api-token-payload.ts +0 -18
- package/src/signer/signer.ts +0 -121
- package/src/token/refresh-token.ts +0 -30
- package/src/token/token-claims.ts +0 -22
- package/src/token/token-data.ts +0 -40
- package/src/token/token-id.ts +0 -25
- package/src/token/token-manager.ts +0 -427
- package/src/token/token-store.ts +0 -88
- package/src/types/authorization-response-error.ts +0 -27
- package/src/types/color-hue.ts +0 -3
- package/src/types/email-otp.ts +0 -3
- package/src/types/email.ts +0 -26
- package/src/types/handle.ts +0 -23
- package/src/types/invite-code.ts +0 -4
- package/src/types/par-response-error.ts +0 -25
- package/src/types/password.ts +0 -4
- package/src/types/rgb-color.ts +0 -21
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -4
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { createReadStream } from 'node:fs'
|
|
2
|
-
import { createRequire } from 'node:module'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
import { Readable } from 'node:stream'
|
|
5
|
-
import type { Manifest } from '@atproto-labs/rollup-plugin-bundle-manifest'
|
|
6
|
-
import { AssetRef } from '../../lib/html/build-document.js'
|
|
7
|
-
import {
|
|
8
|
-
Middleware,
|
|
9
|
-
validateFetchDest,
|
|
10
|
-
validateFetchSite,
|
|
11
|
-
writeStream,
|
|
12
|
-
} from '../../lib/http/index.js'
|
|
13
|
-
|
|
14
|
-
type Asset =
|
|
15
|
-
| {
|
|
16
|
-
type: 'asset'
|
|
17
|
-
mime?: string
|
|
18
|
-
sha256: string
|
|
19
|
-
stream: () => Readable
|
|
20
|
-
}
|
|
21
|
-
| {
|
|
22
|
-
type: 'chunk'
|
|
23
|
-
mime: string
|
|
24
|
-
sha256: string
|
|
25
|
-
dynamicImports: string[]
|
|
26
|
-
isDynamicEntry: boolean
|
|
27
|
-
isEntry: boolean
|
|
28
|
-
isImplicitEntry: boolean
|
|
29
|
-
name: string
|
|
30
|
-
stream: () => Readable
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const ASSETS_URL_PREFIX = '/@atproto/oauth-provider/~assets/'
|
|
34
|
-
|
|
35
|
-
export function parseAssetsManifest(manifestPath: string) {
|
|
36
|
-
// Using `require` instead of `JSON.parse(readFileSync())` so that node's
|
|
37
|
-
// watch mode can pick up changes to the manifest file.
|
|
38
|
-
const require = createRequire(import.meta.url)
|
|
39
|
-
|
|
40
|
-
// eslint-disable-next-line import/no-dynamic-require
|
|
41
|
-
const manifest = require(manifestPath) as Manifest
|
|
42
|
-
|
|
43
|
-
const assets = new Map<string, Asset>(
|
|
44
|
-
Object.entries(manifest).map(([filename, { data, ...item }]) => {
|
|
45
|
-
const buffer = data ? Buffer.from(data, 'base64') : null
|
|
46
|
-
const filepath = join(manifestPath, '..', filename)
|
|
47
|
-
const stream = buffer
|
|
48
|
-
? () => Readable.from(buffer)
|
|
49
|
-
: () => createReadStream(filepath)
|
|
50
|
-
return [filename, { ...item, stream }]
|
|
51
|
-
}),
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
const assetsMiddleware: Middleware = (req, res, next) => {
|
|
55
|
-
if (req.method !== 'GET' && req.method !== 'HEAD') return next()
|
|
56
|
-
if (!req.url?.startsWith(ASSETS_URL_PREFIX)) return next()
|
|
57
|
-
|
|
58
|
-
const filename = decodeURIComponent(req.url.slice(ASSETS_URL_PREFIX.length))
|
|
59
|
-
if (!filename) return next()
|
|
60
|
-
|
|
61
|
-
const asset = assets.get(filename)
|
|
62
|
-
if (!asset) return next()
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
// Allow "null" (ie. no header) to allow loading assets outside of a
|
|
66
|
-
// fetch context (not from a web page).
|
|
67
|
-
validateFetchSite(req, [null, 'none', 'cross-site', 'same-origin'])
|
|
68
|
-
validateFetchDest(req, [null, 'document', 'style', 'script'])
|
|
69
|
-
} catch (err) {
|
|
70
|
-
return next(err)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (req.headers['if-none-match'] === asset.sha256) {
|
|
74
|
-
return void res.writeHead(304).end()
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
res.setHeader('ETag', asset.sha256)
|
|
78
|
-
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
|
|
79
|
-
|
|
80
|
-
writeStream(res, asset.stream(), { contentType: asset.mime })
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
getAssets,
|
|
85
|
-
assetsMiddleware,
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function getAssets(entryName: string) {
|
|
89
|
-
const scripts = getScripts(entryName)
|
|
90
|
-
if (!scripts.length) return null
|
|
91
|
-
const styles = getStyles(entryName)
|
|
92
|
-
return { scripts, styles }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function getScripts(entryName: string) {
|
|
96
|
-
return Array.from(assets)
|
|
97
|
-
.filter(
|
|
98
|
-
([, asset]) =>
|
|
99
|
-
asset.type === 'chunk' && asset.isEntry && asset.name === entryName,
|
|
100
|
-
)
|
|
101
|
-
.map(assetEntryUrl)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function getStyles(_entryName: string) {
|
|
105
|
-
return Array.from(assets)
|
|
106
|
-
.filter(([, asset]) => asset.mime === 'text/css')
|
|
107
|
-
.map(assetEntryUrl)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function assetEntryUrl([filename]: [string, Asset]): AssetRef {
|
|
112
|
-
return { url: assetUrl(filename) }
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function assetUrl(filename: string) {
|
|
116
|
-
return `${ASSETS_URL_PREFIX}${encodeURIComponent(filename)}`
|
|
117
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import { createRequire } from 'node:module'
|
|
3
|
-
import type { HydrationData as UiHydrationData } from '@atproto/oauth-provider-ui/hydration-data'
|
|
4
|
-
import { buildCustomizationCss } from '../../customization/build-customization-css.js'
|
|
5
|
-
import { buildCustomizationData } from '../../customization/build-customization-data.js'
|
|
6
|
-
import { Customization } from '../../customization/customization.js'
|
|
7
|
-
import { CspConfig, mergeCsp } from '../../lib/csp/index.js'
|
|
8
|
-
import { declareHydrationData } from '../../lib/html/hydration-data.js'
|
|
9
|
-
import { cssCode, html } from '../../lib/html/index.js'
|
|
10
|
-
import { WriteResponseOptions } from '../../lib/http/response.js'
|
|
11
|
-
import {
|
|
12
|
-
CrossOriginEmbedderPolicy,
|
|
13
|
-
SecurityHeadersOptions,
|
|
14
|
-
} from '../../lib/http/security-headers.js'
|
|
15
|
-
import { mergeDefaults } from '../../lib/util/object.js'
|
|
16
|
-
import { Simplify } from '../../lib/util/type.js'
|
|
17
|
-
import { WriteHtmlOptions, writeHtml } from '../../lib/write-html.js'
|
|
18
|
-
import { parseAssetsManifest } from './assets-manifest.js'
|
|
19
|
-
import { setupCsrfToken } from './csrf.js'
|
|
20
|
-
|
|
21
|
-
// If the "ui" and "frontend" packages are ever unified, this can be replaced
|
|
22
|
-
// with a single expression:
|
|
23
|
-
//
|
|
24
|
-
// const { getAssets, assetsMiddleware } = parseAssetsManifest(
|
|
25
|
-
// require.resolve('@atproto/oauth-provider-ui/bundle-manifest.json'),
|
|
26
|
-
// )
|
|
27
|
-
|
|
28
|
-
const require = createRequire(import.meta.url)
|
|
29
|
-
const ui = parseAssetsManifest(
|
|
30
|
-
require.resolve('@atproto/oauth-provider-ui/bundle-manifest.json'),
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
type HydrationData = Simplify<UiHydrationData>
|
|
34
|
-
|
|
35
|
-
function getAssets(entryName: keyof HydrationData) {
|
|
36
|
-
const assetRef = ui.getAssets(entryName)
|
|
37
|
-
if (assetRef) return assetRef
|
|
38
|
-
|
|
39
|
-
// Fool-proof. Should never happen.
|
|
40
|
-
throw new Error(`Entry "${entryName}" not found in assets`)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const assetsMiddleware = ui.assetsMiddleware
|
|
44
|
-
|
|
45
|
-
const SPA_CSP: CspConfig = {
|
|
46
|
-
// API calls are made to the same origin
|
|
47
|
-
'connect-src': ["'self'"],
|
|
48
|
-
// Allow loading of PDS logo & User avatars
|
|
49
|
-
'img-src': ['data:', 'https:'],
|
|
50
|
-
// Prevent embedding in iframes
|
|
51
|
-
'frame-ancestors': ["'none'"],
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* @see {@link https://docs.hcaptcha.com/#content-security-policy-settings}
|
|
56
|
-
*/
|
|
57
|
-
const HCAPTCHA_CSP: CspConfig = {
|
|
58
|
-
'script-src': ['https://hcaptcha.com', 'https://*.hcaptcha.com'],
|
|
59
|
-
'frame-src': ['https://hcaptcha.com', 'https://*.hcaptcha.com'],
|
|
60
|
-
'style-src': ['https://hcaptcha.com', 'https://*.hcaptcha.com'],
|
|
61
|
-
'connect-src': ['https://hcaptcha.com', 'https://*.hcaptcha.com'],
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export type SendWebAppOptions = SecurityHeadersOptions & WriteResponseOptions
|
|
65
|
-
|
|
66
|
-
export function sendWebAppFactory<P extends keyof HydrationData>(
|
|
67
|
-
page: P,
|
|
68
|
-
customization: Customization,
|
|
69
|
-
defaults: SendWebAppOptions = {},
|
|
70
|
-
) {
|
|
71
|
-
// Pre-computed options:
|
|
72
|
-
const customizationData = buildCustomizationData(customization)
|
|
73
|
-
const customizationCss = cssCode(buildCustomizationCss(customization))
|
|
74
|
-
const { scripts, styles } = getAssets(page)
|
|
75
|
-
|
|
76
|
-
const csp = mergeCsp(
|
|
77
|
-
SPA_CSP,
|
|
78
|
-
customization.hcaptcha ? HCAPTCHA_CSP : undefined,
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
const coep = customization.hcaptcha
|
|
82
|
-
? // hCaptcha's implementation of COEP is currently broken. Let's disable it
|
|
83
|
-
// to avoid breaking the entire page.
|
|
84
|
-
//
|
|
85
|
-
// https://github.com/hCaptcha/react-hcaptcha/issues/259
|
|
86
|
-
// https://github.com/hCaptcha/react-hcaptcha/issues/380
|
|
87
|
-
CrossOriginEmbedderPolicy.unsafeNone
|
|
88
|
-
: // Since we are loading avatars form other origins, which might not have
|
|
89
|
-
// CORP headers, we need to use the "credentialless" value, which allows
|
|
90
|
-
// loading cross-origin resources without credentials (cookies, client
|
|
91
|
-
// certificates, etc.). This is a more secure alternative to
|
|
92
|
-
// "unsafe-none". Ideally, we would want to set COEP to "require-corp" and
|
|
93
|
-
// ensure that all cross-origin resources have the appropriate CORP
|
|
94
|
-
// headers.
|
|
95
|
-
CrossOriginEmbedderPolicy.credentialless
|
|
96
|
-
|
|
97
|
-
return async function sendWebApp(
|
|
98
|
-
req: IncomingMessage,
|
|
99
|
-
res: ServerResponse,
|
|
100
|
-
options: SendWebAppOptions & {
|
|
101
|
-
data: Omit<HydrationData[P], '__customizationData'>
|
|
102
|
-
},
|
|
103
|
-
): Promise<void> {
|
|
104
|
-
await setupCsrfToken(req, res)
|
|
105
|
-
|
|
106
|
-
const script = declareHydrationData({
|
|
107
|
-
...options.data,
|
|
108
|
-
__customizationData: customizationData,
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
return writeHtml(
|
|
112
|
-
res,
|
|
113
|
-
mergeDefaults<WriteHtmlOptions>(defaults, options, {
|
|
114
|
-
bodyAttrs: { class: 'text-text-default bg-contrast-0' },
|
|
115
|
-
csp: options?.csp ? mergeCsp(csp, options.csp) : csp,
|
|
116
|
-
coep: options?.coep ?? coep,
|
|
117
|
-
meta: [{ name: 'robots', content: 'noindex' }],
|
|
118
|
-
body: html`<div id="root"></div>`,
|
|
119
|
-
scripts: [script, ...scripts],
|
|
120
|
-
styles: [...styles, customizationCss],
|
|
121
|
-
}),
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import createHttpError from 'http-errors'
|
|
3
|
-
import { CSRF_COOKIE_NAME, CSRF_HEADER_NAME } from '@atproto/oauth-provider-api'
|
|
4
|
-
import {
|
|
5
|
-
CookieSerializeOptions,
|
|
6
|
-
getCookie,
|
|
7
|
-
setCookie,
|
|
8
|
-
} from '../../lib/http/index.js'
|
|
9
|
-
import { randomHexId } from '../../lib/util/crypto.js'
|
|
10
|
-
|
|
11
|
-
const TOKEN_BYTE_LENGTH = 12
|
|
12
|
-
const TOKEN_LENGTH = TOKEN_BYTE_LENGTH * 2 // 2 hex chars per byte
|
|
13
|
-
|
|
14
|
-
// @NOTE Cookie based CSRF protection is redundant with session cookies using
|
|
15
|
-
// `SameSite` and could probably be removed in the future.
|
|
16
|
-
const CSRF_COOKIE_OPTIONS: Readonly<CookieSerializeOptions> = {
|
|
17
|
-
expires: undefined, // "session" cookie
|
|
18
|
-
secure: true,
|
|
19
|
-
httpOnly: false, // Need to be accessible from JavaScript
|
|
20
|
-
sameSite: 'lax',
|
|
21
|
-
path: `/`,
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function generateCsrfToken() {
|
|
25
|
-
return randomHexId(TOKEN_BYTE_LENGTH)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function setupCsrfToken(
|
|
29
|
-
req: IncomingMessage,
|
|
30
|
-
res: ServerResponse,
|
|
31
|
-
): Promise<void> {
|
|
32
|
-
const token = getCookieCsrf(req) || (await generateCsrfToken())
|
|
33
|
-
|
|
34
|
-
// Refresh cookie (See Chrome's "Lax+POST" behavior)
|
|
35
|
-
setCookie(res, CSRF_COOKIE_NAME, token, CSRF_COOKIE_OPTIONS)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function validateCsrfToken(
|
|
39
|
-
req: IncomingMessage,
|
|
40
|
-
res: ServerResponse,
|
|
41
|
-
) {
|
|
42
|
-
const cookieValue = getCookieCsrf(req)
|
|
43
|
-
const headerValue = getHeadersCsrf(req)
|
|
44
|
-
|
|
45
|
-
// Refresh cookie (See Chrome's "Lax+POST" behavior), or set a new one,
|
|
46
|
-
// allowing clients to retry with the new token.
|
|
47
|
-
setCookie(
|
|
48
|
-
res,
|
|
49
|
-
CSRF_COOKIE_NAME,
|
|
50
|
-
cookieValue || (await generateCsrfToken()),
|
|
51
|
-
CSRF_COOKIE_OPTIONS,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
if (!headerValue) {
|
|
55
|
-
throw createHttpError(400, `Missing CSRF header`)
|
|
56
|
-
}
|
|
57
|
-
if (!cookieValue) {
|
|
58
|
-
throw createHttpError(400, `Missing CSRF cookie`)
|
|
59
|
-
}
|
|
60
|
-
if (cookieValue !== headerValue) {
|
|
61
|
-
throw createHttpError(400, `CSRF mismatch`)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function getCookieCsrf(req: IncomingMessage) {
|
|
66
|
-
const cookieValue = getCookie(req, CSRF_COOKIE_NAME)
|
|
67
|
-
if (cookieValue?.length === TOKEN_LENGTH) {
|
|
68
|
-
return cookieValue
|
|
69
|
-
}
|
|
70
|
-
return undefined
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function getHeadersCsrf(req: IncomingMessage) {
|
|
74
|
-
const headerValue = req.headers[CSRF_HEADER_NAME]
|
|
75
|
-
if (typeof headerValue === 'string' && headerValue.length === TOKEN_LENGTH) {
|
|
76
|
-
return headerValue
|
|
77
|
-
}
|
|
78
|
-
return undefined
|
|
79
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import type { ActiveDeviceSession } from '@atproto/oauth-provider-api'
|
|
3
|
-
import { Customization } from '../../customization/customization.js'
|
|
4
|
-
import { SendWebAppOptions, sendWebAppFactory } from './assets.js'
|
|
5
|
-
|
|
6
|
-
export function sendAccountPageFactory(
|
|
7
|
-
customization: Customization,
|
|
8
|
-
options?: SendWebAppOptions,
|
|
9
|
-
) {
|
|
10
|
-
const sendApp = sendWebAppFactory('account-page', customization, options)
|
|
11
|
-
|
|
12
|
-
return async function sendAccountPage(
|
|
13
|
-
req: IncomingMessage,
|
|
14
|
-
res: ServerResponse,
|
|
15
|
-
data: {
|
|
16
|
-
deviceSessions: readonly ActiveDeviceSession[]
|
|
17
|
-
},
|
|
18
|
-
): Promise<void> {
|
|
19
|
-
return sendApp(req, res, {
|
|
20
|
-
data: { __deviceSessions: data.deviceSessions },
|
|
21
|
-
})
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import { Customization } from '../../customization/customization.js'
|
|
3
|
-
import { AuthorizationResultAuthorizePage } from '../../result/authorization-result-authorize-page.js'
|
|
4
|
-
import { SendWebAppOptions, sendWebAppFactory } from './assets.js'
|
|
5
|
-
|
|
6
|
-
export function sendAuthorizePageFactory(
|
|
7
|
-
customization: Customization,
|
|
8
|
-
options?: SendWebAppOptions,
|
|
9
|
-
) {
|
|
10
|
-
const sendApp = sendWebAppFactory(
|
|
11
|
-
'authorization-page',
|
|
12
|
-
customization,
|
|
13
|
-
options,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
return async function sendAuthorizePage(
|
|
17
|
-
req: IncomingMessage,
|
|
18
|
-
res: ServerResponse,
|
|
19
|
-
data: AuthorizationResultAuthorizePage,
|
|
20
|
-
): Promise<void> {
|
|
21
|
-
return sendApp(req, res, {
|
|
22
|
-
data: {
|
|
23
|
-
__authorizeData: {
|
|
24
|
-
requestUri: data.requestUri,
|
|
25
|
-
|
|
26
|
-
clientId: data.client.id,
|
|
27
|
-
clientMetadata: data.client.metadata,
|
|
28
|
-
clientTrusted: data.client.info.isTrusted,
|
|
29
|
-
clientFirstParty: data.client.info.isFirstParty,
|
|
30
|
-
|
|
31
|
-
scope: data.parameters.scope,
|
|
32
|
-
uiLocales: data.parameters.ui_locales,
|
|
33
|
-
loginHint: data.parameters.login_hint,
|
|
34
|
-
promptMode: data.parameters.prompt,
|
|
35
|
-
permissionSets: Object.fromEntries(data.permissionSets),
|
|
36
|
-
selectedDid: data.selectedDid,
|
|
37
|
-
},
|
|
38
|
-
__sessions: data.sessions,
|
|
39
|
-
},
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import { Customization } from '../../customization/customization.js'
|
|
3
|
-
import { SendWebAppOptions, sendWebAppFactory } from './assets.js'
|
|
4
|
-
|
|
5
|
-
export function sendCookieErrorPageFactory(
|
|
6
|
-
customization: Customization,
|
|
7
|
-
options?: SendWebAppOptions,
|
|
8
|
-
) {
|
|
9
|
-
const sendApp = sendWebAppFactory('cookie-error-page', customization, options)
|
|
10
|
-
|
|
11
|
-
return async function sendCookieErrorPage(
|
|
12
|
-
req: IncomingMessage,
|
|
13
|
-
res: ServerResponse,
|
|
14
|
-
data: { continueUrl: URL },
|
|
15
|
-
) {
|
|
16
|
-
return sendApp(req, res, {
|
|
17
|
-
status: 400,
|
|
18
|
-
data: { __continueUrl: data.continueUrl.toString() },
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import { Customization } from '../../customization/customization.js'
|
|
3
|
-
import {
|
|
4
|
-
buildErrorPayload,
|
|
5
|
-
buildErrorStatus,
|
|
6
|
-
} from '../../errors/error-parser.js'
|
|
7
|
-
import { SendWebAppOptions, sendWebAppFactory } from './assets.js'
|
|
8
|
-
|
|
9
|
-
export function sendErrorPageFactory(
|
|
10
|
-
customization: Customization,
|
|
11
|
-
options?: SendWebAppOptions,
|
|
12
|
-
) {
|
|
13
|
-
const sendApp = sendWebAppFactory('error-page', customization, options)
|
|
14
|
-
|
|
15
|
-
return async function sendErrorPage(
|
|
16
|
-
req: IncomingMessage,
|
|
17
|
-
res: ServerResponse,
|
|
18
|
-
err: unknown,
|
|
19
|
-
): Promise<void> {
|
|
20
|
-
return sendApp(req, res, {
|
|
21
|
-
status: buildErrorStatus(err),
|
|
22
|
-
data: { __errorData: buildErrorPayload(err) },
|
|
23
|
-
})
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import type { ServerResponse } from 'node:http'
|
|
2
|
-
import {
|
|
3
|
-
OAuthAuthorizationRequestParameters,
|
|
4
|
-
OAuthResponseMode,
|
|
5
|
-
} from '@atproto/oauth-types'
|
|
6
|
-
import { AuthorizationError } from '../../errors/authorization-error.js'
|
|
7
|
-
import {
|
|
8
|
-
WriteFormRedirectOptions,
|
|
9
|
-
writeFormRedirect,
|
|
10
|
-
} from '../../lib/write-form-redirect.js'
|
|
11
|
-
import { AuthorizationRedirectParameters } from '../../result/authorization-redirect-parameters.js'
|
|
12
|
-
import { AuthorizationResultRedirect } from '../../result/authorization-result-redirect.js'
|
|
13
|
-
|
|
14
|
-
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-7.5.4
|
|
15
|
-
const REDIRECT_STATUS_CODE = 303
|
|
16
|
-
|
|
17
|
-
export const SUCCESS_REDIRECT_KEYS = [
|
|
18
|
-
'code',
|
|
19
|
-
'id_token',
|
|
20
|
-
'access_token',
|
|
21
|
-
'expires_in',
|
|
22
|
-
'token_type',
|
|
23
|
-
] as const
|
|
24
|
-
|
|
25
|
-
export const ERROR_REDIRECT_KEYS = [
|
|
26
|
-
'error',
|
|
27
|
-
'error_description',
|
|
28
|
-
'error_uri',
|
|
29
|
-
] as const
|
|
30
|
-
|
|
31
|
-
export type OAuthRedirectQueryParameter =
|
|
32
|
-
| 'iss'
|
|
33
|
-
| 'state'
|
|
34
|
-
| (typeof SUCCESS_REDIRECT_KEYS)[number]
|
|
35
|
-
| (typeof ERROR_REDIRECT_KEYS)[number]
|
|
36
|
-
|
|
37
|
-
export function buildRedirectUri(
|
|
38
|
-
parameters: OAuthAuthorizationRequestParameters,
|
|
39
|
-
): string {
|
|
40
|
-
const uri = parameters.redirect_uri
|
|
41
|
-
if (uri) return uri
|
|
42
|
-
|
|
43
|
-
throw new AuthorizationError(parameters, 'No redirect_uri', 'invalid_request')
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function buildRedirectMode(
|
|
47
|
-
parameters: OAuthAuthorizationRequestParameters,
|
|
48
|
-
): OAuthResponseMode {
|
|
49
|
-
const mode = parameters.response_mode || 'query' // @TODO default should depend on response_type
|
|
50
|
-
return mode
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function buildRedirectParams(
|
|
54
|
-
issuer: string,
|
|
55
|
-
parameters: OAuthAuthorizationRequestParameters,
|
|
56
|
-
redirect: AuthorizationRedirectParameters,
|
|
57
|
-
): [OAuthRedirectQueryParameter, string][] {
|
|
58
|
-
const params: [OAuthRedirectQueryParameter, string][] = [
|
|
59
|
-
['iss', issuer], // rfc9207
|
|
60
|
-
]
|
|
61
|
-
|
|
62
|
-
if (parameters.state != null) {
|
|
63
|
-
params.push(['state', parameters.state])
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const keys = 'code' in redirect ? SUCCESS_REDIRECT_KEYS : ERROR_REDIRECT_KEYS
|
|
67
|
-
for (const key of keys) {
|
|
68
|
-
const value = redirect[key]
|
|
69
|
-
if (value != null) params.push([key, value])
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return params
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function sendAuthorizationResultRedirect(
|
|
76
|
-
res: ServerResponse,
|
|
77
|
-
result: AuthorizationResultRedirect,
|
|
78
|
-
options?: WriteFormRedirectOptions,
|
|
79
|
-
) {
|
|
80
|
-
const { issuer, parameters, redirect } = result
|
|
81
|
-
|
|
82
|
-
return sendRedirect(
|
|
83
|
-
res,
|
|
84
|
-
{
|
|
85
|
-
redirectUri: buildRedirectUri(parameters),
|
|
86
|
-
mode: buildRedirectMode(parameters),
|
|
87
|
-
params: buildRedirectParams(issuer, parameters, redirect),
|
|
88
|
-
},
|
|
89
|
-
options,
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export type OAuthRedirectOptions = {
|
|
94
|
-
mode: OAuthResponseMode
|
|
95
|
-
redirectUri: string
|
|
96
|
-
params: Iterable<[string, string]>
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function sendRedirect(
|
|
100
|
-
res: ServerResponse,
|
|
101
|
-
redirect: OAuthRedirectOptions,
|
|
102
|
-
options?: WriteFormRedirectOptions,
|
|
103
|
-
): void {
|
|
104
|
-
res.setHeader('Cache-Control', 'no-store')
|
|
105
|
-
|
|
106
|
-
const { mode, redirectUri: uri, params } = redirect
|
|
107
|
-
switch (mode) {
|
|
108
|
-
case 'query':
|
|
109
|
-
return writeQuery(res, uri, params)
|
|
110
|
-
case 'fragment':
|
|
111
|
-
return writeFragment(res, uri, params)
|
|
112
|
-
case 'form_post':
|
|
113
|
-
return writeFormRedirect(res, 'post', uri, params, options)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// @ts-expect-error fool proof
|
|
117
|
-
throw new Error(`Unsupported mode: ${mode}`)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function writeQuery(
|
|
121
|
-
res: ServerResponse,
|
|
122
|
-
uri: string,
|
|
123
|
-
params: Iterable<[string, string]>,
|
|
124
|
-
): void {
|
|
125
|
-
const url = new URL(uri)
|
|
126
|
-
for (const [key, value] of params) url.searchParams.set(key, value)
|
|
127
|
-
res.writeHead(REDIRECT_STATUS_CODE, { Location: url.href }).end()
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function writeFragment(
|
|
131
|
-
res: ServerResponse,
|
|
132
|
-
uri: string,
|
|
133
|
-
params: Iterable<[string, string]>,
|
|
134
|
-
): void {
|
|
135
|
-
const url = new URL(uri)
|
|
136
|
-
const searchParams = new URLSearchParams()
|
|
137
|
-
for (const [key, value] of params) searchParams.set(key, value)
|
|
138
|
-
url.hash = searchParams.toString()
|
|
139
|
-
res.writeHead(REDIRECT_STATUS_CODE, { Location: url.href }).end()
|
|
140
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import type { ActiveDeviceSession } from '@atproto/oauth-provider-api'
|
|
3
|
-
import {
|
|
4
|
-
Middleware,
|
|
5
|
-
Router,
|
|
6
|
-
validateFetchDest,
|
|
7
|
-
validateFetchMode,
|
|
8
|
-
validateOrigin,
|
|
9
|
-
writeRedirect,
|
|
10
|
-
} from '../lib/http/index.js'
|
|
11
|
-
import { SecurityHeadersOptions } from '../lib/http/security-headers.js'
|
|
12
|
-
import type { OAuthProvider } from '../oauth-provider.js'
|
|
13
|
-
import { sendAccountPageFactory } from './assets/send-account-page.js'
|
|
14
|
-
import { sendErrorPageFactory } from './assets/send-error-page.js'
|
|
15
|
-
import type { MiddlewareOptions } from './middleware-options.js'
|
|
16
|
-
|
|
17
|
-
export function createAccountPageMiddleware<
|
|
18
|
-
Ctx extends object | void = void,
|
|
19
|
-
Req extends IncomingMessage = IncomingMessage,
|
|
20
|
-
Res extends ServerResponse = ServerResponse,
|
|
21
|
-
>(
|
|
22
|
-
server: OAuthProvider,
|
|
23
|
-
{ onError }: MiddlewareOptions<Req, Res>,
|
|
24
|
-
): Middleware<Ctx, Req, Res> {
|
|
25
|
-
const issuerUrl = new URL(server.issuer)
|
|
26
|
-
const issuerOrigin = issuerUrl.origin
|
|
27
|
-
|
|
28
|
-
const securityOptions: SecurityHeadersOptions = {
|
|
29
|
-
hsts: issuerUrl.protocol === 'http:' ? false : undefined,
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const sendAccountPage = sendAccountPageFactory(
|
|
33
|
-
server.customization,
|
|
34
|
-
securityOptions,
|
|
35
|
-
)
|
|
36
|
-
const sendErrorPage = sendErrorPageFactory(
|
|
37
|
-
server.customization,
|
|
38
|
-
securityOptions,
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
const router = new Router<Ctx, Req, Res>(issuerUrl)
|
|
42
|
-
|
|
43
|
-
// Create password reset discovery endpoint
|
|
44
|
-
// https://www.w3.org/TR/change-password-url/
|
|
45
|
-
router.get('/.well-known/change-password', (_req, res) => {
|
|
46
|
-
writeRedirect(res, new URL('/account/reset-password', issuerUrl).toString())
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
// Create frontend account pages
|
|
50
|
-
router.get<never>(/^\/account(?:\/.*)?$/, async function (req, res) {
|
|
51
|
-
try {
|
|
52
|
-
res.setHeader('Referrer-Policy', 'same-origin')
|
|
53
|
-
|
|
54
|
-
res.setHeader('Cache-Control', 'no-store')
|
|
55
|
-
res.setHeader('Pragma', 'no-cache')
|
|
56
|
-
|
|
57
|
-
validateFetchMode(req, ['navigate'])
|
|
58
|
-
validateFetchDest(req, ['document'])
|
|
59
|
-
validateOrigin(req, issuerOrigin)
|
|
60
|
-
|
|
61
|
-
const { deviceId } = await server.deviceManager.load(req, res)
|
|
62
|
-
const deviceAccounts =
|
|
63
|
-
await server.accountManager.listDeviceAccounts(deviceId)
|
|
64
|
-
|
|
65
|
-
sendAccountPage(req, res, {
|
|
66
|
-
deviceSessions: deviceAccounts.map(
|
|
67
|
-
(deviceAccount): ActiveDeviceSession => ({
|
|
68
|
-
account: deviceAccount.account,
|
|
69
|
-
loginRequired: server.checkLoginRequired(deviceAccount),
|
|
70
|
-
}),
|
|
71
|
-
),
|
|
72
|
-
})
|
|
73
|
-
} catch (err) {
|
|
74
|
-
onError?.(
|
|
75
|
-
req,
|
|
76
|
-
res,
|
|
77
|
-
err,
|
|
78
|
-
`Failed to handle navigation request to "${req.url}"`,
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
if (!res.headersSent) {
|
|
82
|
-
return sendErrorPage(req, res, err)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
return router.buildMiddleware()
|
|
88
|
-
}
|