@atproto/oauth-provider 0.19.6 → 0.19.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/errors/error-parser.js +5 -5
  3. package/dist/errors/error-parser.js.map +1 -1
  4. package/package.json +22 -18
  5. package/src/access-token/access-token-mode.ts +0 -4
  6. package/src/account/account-manager.ts +0 -591
  7. package/src/account/account-store.ts +0 -305
  8. package/src/account/sign-in-data.ts +0 -15
  9. package/src/account/sign-up-input.ts +0 -20
  10. package/src/client/client-auth.ts +0 -64
  11. package/src/client/client-data.ts +0 -9
  12. package/src/client/client-id.ts +0 -4
  13. package/src/client/client-info.ts +0 -13
  14. package/src/client/client-manager.ts +0 -772
  15. package/src/client/client-store.ts +0 -35
  16. package/src/client/client-utils.ts +0 -37
  17. package/src/client/client.ts +0 -394
  18. package/src/constants.ts +0 -80
  19. package/src/customization/branding.ts +0 -12
  20. package/src/customization/build-customization-css.ts +0 -55
  21. package/src/customization/build-customization-data.ts +0 -24
  22. package/src/customization/colors.ts +0 -48
  23. package/src/customization/customization.ts +0 -29
  24. package/src/customization/links.ts +0 -10
  25. package/src/device/device-data.ts +0 -11
  26. package/src/device/device-id.ts +0 -27
  27. package/src/device/device-manager.ts +0 -292
  28. package/src/device/device-store.ts +0 -31
  29. package/src/device/session-id.ts +0 -21
  30. package/src/dpop/dpop-manager.ts +0 -214
  31. package/src/dpop/dpop-nonce.ts +0 -121
  32. package/src/dpop/dpop-proof.ts +0 -6
  33. package/src/errors/access-denied-error.ts +0 -12
  34. package/src/errors/account-selection-required-error.ts +0 -12
  35. package/src/errors/authorization-error.ts +0 -45
  36. package/src/errors/consent-required-error.ts +0 -12
  37. package/src/errors/error-parser.ts +0 -147
  38. package/src/errors/handle-unavailable-error.ts +0 -23
  39. package/src/errors/invalid-authorization-details-error.ts +0 -27
  40. package/src/errors/invalid-client-error.ts +0 -20
  41. package/src/errors/invalid-client-id-error.ts +0 -31
  42. package/src/errors/invalid-client-metadata-error.ts +0 -68
  43. package/src/errors/invalid-credentials-error.ts +0 -29
  44. package/src/errors/invalid-dpop-key-binding-error.ts +0 -21
  45. package/src/errors/invalid-dpop-proof-error.ts +0 -13
  46. package/src/errors/invalid-grant-error.ts +0 -21
  47. package/src/errors/invalid-invite-code-error.ts +0 -10
  48. package/src/errors/invalid-redirect-uri-error.ts +0 -17
  49. package/src/errors/invalid-request-error.ts +0 -32
  50. package/src/errors/invalid-scope-error.ts +0 -15
  51. package/src/errors/invalid-token-error.ts +0 -60
  52. package/src/errors/login-required-error.ts +0 -12
  53. package/src/errors/oauth-error.ts +0 -28
  54. package/src/errors/second-authentication-factor-required-error.ts +0 -25
  55. package/src/errors/unauthorized-client-error.ts +0 -20
  56. package/src/errors/use-dpop-nonce-error.ts +0 -32
  57. package/src/errors/www-authenticate-error.ts +0 -64
  58. package/src/index.ts +0 -16
  59. package/src/lexicon/lexicon-data.ts +0 -11
  60. package/src/lexicon/lexicon-getter.ts +0 -55
  61. package/src/lexicon/lexicon-manager.ts +0 -116
  62. package/src/lexicon/lexicon-store.ts +0 -35
  63. package/src/lib/csp/index.ts +0 -101
  64. package/src/lib/hcaptcha.ts +0 -228
  65. package/src/lib/html/README.md +0 -9
  66. package/src/lib/html/build-document.ts +0 -151
  67. package/src/lib/html/escapers.ts +0 -66
  68. package/src/lib/html/html.ts +0 -56
  69. package/src/lib/html/hydration-data.ts +0 -19
  70. package/src/lib/html/index.ts +0 -5
  71. package/src/lib/html/tags.ts +0 -67
  72. package/src/lib/html/util.ts +0 -17
  73. package/src/lib/http/README.md +0 -11
  74. package/src/lib/http/accept.ts +0 -90
  75. package/src/lib/http/context.ts +0 -42
  76. package/src/lib/http/headers.ts +0 -15
  77. package/src/lib/http/index.ts +0 -10
  78. package/src/lib/http/method.ts +0 -18
  79. package/src/lib/http/middleware.ts +0 -169
  80. package/src/lib/http/parser.ts +0 -101
  81. package/src/lib/http/path.ts +0 -82
  82. package/src/lib/http/request.ts +0 -256
  83. package/src/lib/http/response.ts +0 -122
  84. package/src/lib/http/route.ts +0 -60
  85. package/src/lib/http/router.ts +0 -117
  86. package/src/lib/http/security-headers.ts +0 -100
  87. package/src/lib/http/stream.ts +0 -57
  88. package/src/lib/http/types.ts +0 -16
  89. package/src/lib/http/url.ts +0 -23
  90. package/src/lib/nsid.ts +0 -10
  91. package/src/lib/redis.ts +0 -25
  92. package/src/lib/util/authorization-header.ts +0 -28
  93. package/src/lib/util/cast.ts +0 -18
  94. package/src/lib/util/color.ts +0 -168
  95. package/src/lib/util/crypto.ts +0 -32
  96. package/src/lib/util/date.ts +0 -7
  97. package/src/lib/util/error.ts +0 -7
  98. package/src/lib/util/function.ts +0 -39
  99. package/src/lib/util/locale.ts +0 -12
  100. package/src/lib/util/object.ts +0 -23
  101. package/src/lib/util/redirect-uri.ts +0 -46
  102. package/src/lib/util/time.ts +0 -49
  103. package/src/lib/util/type.ts +0 -182
  104. package/src/lib/util/ui8.ts +0 -14
  105. package/src/lib/util/well-known.ts +0 -8
  106. package/src/lib/util/zod-error.ts +0 -26
  107. package/src/lib/write-form-redirect.ts +0 -58
  108. package/src/lib/write-html.ts +0 -70
  109. package/src/metadata/build-metadata.ts +0 -141
  110. package/src/oauth-client.ts +0 -3
  111. package/src/oauth-dpop.ts +0 -2
  112. package/src/oauth-errors.ts +0 -26
  113. package/src/oauth-hooks.ts +0 -519
  114. package/src/oauth-middleware.ts +0 -53
  115. package/src/oauth-provider.ts +0 -1110
  116. package/src/oauth-store.ts +0 -12
  117. package/src/oauth-verifier.ts +0 -260
  118. package/src/replay/replay-manager.ts +0 -51
  119. package/src/replay/replay-store-memory.ts +0 -36
  120. package/src/replay/replay-store-redis.ts +0 -30
  121. package/src/replay/replay-store.ts +0 -44
  122. package/src/request/code.ts +0 -23
  123. package/src/request/request-data.ts +0 -36
  124. package/src/request/request-id.ts +0 -22
  125. package/src/request/request-manager.ts +0 -521
  126. package/src/request/request-store.ts +0 -60
  127. package/src/request/request-uri.ts +0 -42
  128. package/src/result/authorization-redirect-parameters.ts +0 -24
  129. package/src/result/authorization-result-authorize-page.ts +0 -16
  130. package/src/result/authorization-result-redirect.ts +0 -8
  131. package/src/router/assets/assets-manifest.ts +0 -117
  132. package/src/router/assets/assets.ts +0 -124
  133. package/src/router/assets/csrf.ts +0 -79
  134. package/src/router/assets/send-account-page.ts +0 -23
  135. package/src/router/assets/send-authorization-page.ts +0 -42
  136. package/src/router/assets/send-cookie-error-page.ts +0 -21
  137. package/src/router/assets/send-error-page.ts +0 -25
  138. package/src/router/assets/send-redirect.ts +0 -140
  139. package/src/router/create-account-page-middleware.ts +0 -88
  140. package/src/router/create-api-middleware.ts +0 -1051
  141. package/src/router/create-authorization-page-middleware.ts +0 -281
  142. package/src/router/create-oauth-middleware.ts +0 -257
  143. package/src/router/error-handler.ts +0 -6
  144. package/src/router/middleware-options.ts +0 -9
  145. package/src/signer/access-token-payload.ts +0 -25
  146. package/src/signer/api-token-payload.ts +0 -18
  147. package/src/signer/signer.ts +0 -121
  148. package/src/token/refresh-token.ts +0 -30
  149. package/src/token/token-claims.ts +0 -22
  150. package/src/token/token-data.ts +0 -40
  151. package/src/token/token-id.ts +0 -25
  152. package/src/token/token-manager.ts +0 -427
  153. package/src/token/token-store.ts +0 -88
  154. package/src/types/authorization-response-error.ts +0 -27
  155. package/src/types/color-hue.ts +0 -3
  156. package/src/types/email-otp.ts +0 -3
  157. package/src/types/email.ts +0 -26
  158. package/src/types/handle.ts +0 -23
  159. package/src/types/invite-code.ts +0 -4
  160. package/src/types/par-response-error.ts +0 -25
  161. package/src/types/password.ts +0 -4
  162. package/src/types/rgb-color.ts +0 -21
  163. package/tsconfig.build.json +0 -8
  164. package/tsconfig.build.tsbuildinfo +0 -1
  165. package/tsconfig.json +0 -4
@@ -1,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
- }