@growth-labs/auth 0.10.0 → 0.10.3
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/README.md +17 -5
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +12 -1
- package/dist/config-store.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +23 -3
- package/dist/middleware/auth.js.map +1 -1
- package/dist/options.d.ts +4 -3
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +15 -1
- package/dist/options.js.map +1 -1
- package/dist/renderers.d.ts +8 -0
- package/dist/renderers.d.ts.map +1 -1
- package/dist/renderers.js +27 -2
- package/dist/renderers.js.map +1 -1
- package/dist/routes/api/code-request.d.ts.map +1 -1
- package/dist/routes/api/code-request.js +5 -1
- package/dist/routes/api/code-request.js.map +1 -1
- package/dist/routes/api/code-verify.d.ts.map +1 -1
- package/dist/routes/api/code-verify.js +5 -1
- package/dist/routes/api/code-verify.js.map +1 -1
- package/dist/routes/api/password-reset-confirm.d.ts.map +1 -1
- package/dist/routes/api/password-reset-confirm.js +5 -1
- package/dist/routes/api/password-reset-confirm.js.map +1 -1
- package/dist/routes/api/password-reset-request.d.ts.map +1 -1
- package/dist/routes/api/password-reset-request.js +5 -1
- package/dist/routes/api/password-reset-request.js.map +1 -1
- package/dist/routes/api/password-verify.d.ts.map +1 -1
- package/dist/routes/api/password-verify.js +5 -1
- package/dist/routes/api/password-verify.js.map +1 -1
- package/dist/routes/api/shared.d.ts +1 -0
- package/dist/routes/api/shared.d.ts.map +1 -1
- package/dist/routes/api/shared.js +26 -2
- package/dist/routes/api/shared.js.map +1 -1
- package/dist/routes/callback.d.ts.map +1 -1
- package/dist/routes/callback.js +12 -3
- package/dist/routes/callback.js.map +1 -1
- package/dist/routes/login.js +4 -4
- package/dist/routes/login.js.map +1 -1
- package/dist/routes/logout.d.ts.map +1 -1
- package/dist/routes/logout.js +35 -6
- package/dist/routes/logout.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +15 -4
- package/dist/server.js.map +1 -1
- package/dist/utils/client-secret-literal.d.ts +13 -0
- package/dist/utils/client-secret-literal.d.ts.map +1 -0
- package/dist/utils/client-secret-literal.js +50 -0
- package/dist/utils/client-secret-literal.js.map +1 -0
- package/dist/utils/client-secret.d.ts +12 -1
- package/dist/utils/client-secret.d.ts.map +1 -1
- package/dist/utils/client-secret.js +108 -14
- package/dist/utils/client-secret.js.map +1 -1
- package/dist/utils/csp.d.ts +83 -0
- package/dist/utils/csp.d.ts.map +1 -0
- package/dist/utils/csp.js +149 -0
- package/dist/utils/csp.js.map +1 -0
- package/dist/utils/fail-closed.d.ts +14 -0
- package/dist/utils/fail-closed.d.ts.map +1 -0
- package/dist/utils/fail-closed.js +53 -0
- package/dist/utils/fail-closed.js.map +1 -0
- package/dist/utils/index.d.ts +5 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/openauth.d.ts +7 -1
- package/dist/utils/openauth.d.ts.map +1 -1
- package/dist/utils/openauth.js +14 -5
- package/dist/utils/openauth.js.map +1 -1
- package/package.json +2 -2
- package/src/config-store.ts +13 -1
- package/src/index.ts +11 -1
- package/src/middleware/auth.ts +30 -3
- package/src/options.ts +16 -1
- package/src/renderers.ts +28 -2
- package/src/routes/api/code-request.ts +5 -0
- package/src/routes/api/code-verify.ts +5 -0
- package/src/routes/api/password-reset-confirm.ts +5 -0
- package/src/routes/api/password-reset-request.ts +5 -0
- package/src/routes/api/password-verify.ts +5 -0
- package/src/routes/api/shared.ts +33 -2
- package/src/routes/callback.ts +12 -2
- package/src/routes/login.ts +4 -4
- package/src/routes/logout.ts +35 -6
- package/src/server.ts +21 -4
- package/src/utils/client-secret-literal.ts +71 -0
- package/src/utils/client-secret.ts +132 -18
- package/src/utils/csp.ts +167 -0
- package/src/utils/fail-closed.ts +74 -0
- package/src/utils/index.ts +22 -1
- package/src/utils/openauth.ts +29 -5
package/src/utils/csp.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-contained Content-Security-Policy for auth renderer pages.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. The auth integration serves its UI surfaces (`/login`,
|
|
5
|
+
* `/verify-code`, `/password`, `/forgot-password`, `/reset-password`) from
|
|
6
|
+
* `injectRoute`-registered Astro endpoints that return a raw `Response` built
|
|
7
|
+
* from an HTML string. Astro's native hash-based `security.csp` only rewrites
|
|
8
|
+
* *component-rendered* pages — it scans the rendered document, hashes inline
|
|
9
|
+
* `<script>`/`<style>`, and injects a `<meta http-equiv>` CSP. An endpoint that
|
|
10
|
+
* returns its own `Response` bypasses that pass entirely, so on a site with an
|
|
11
|
+
* enforced `security.csp` the auth pages ship transport headers but NO CSP and
|
|
12
|
+
* their inline theme-bootstrap script is un-hashed (fulcrum-labs/warfronts
|
|
13
|
+
* verified live: `/login` 200, no `Content-Security-Policy` header or meta).
|
|
14
|
+
*
|
|
15
|
+
* This is the auth-side analogue of the `@growth-labs/consent@0.7.1`
|
|
16
|
+
* `context.csp.insertScriptHash` fix. Consent registers its banner hash and
|
|
17
|
+
* lets Astro render the CSP meta into the `.astro` page; auth has no Astro page
|
|
18
|
+
* to carry a meta, so it computes the sha256 hashes of its own inline scripts
|
|
19
|
+
* and styles and emits the `<meta http-equiv="Content-Security-Policy">` itself.
|
|
20
|
+
* Browsers combine a CSP meta with any CSP response header by intersection, so a
|
|
21
|
+
* site keeps owning the transport-header CSP (or none) while auth owns the meta
|
|
22
|
+
* on its own surfaces — exactly the model warfronts documents (CSP lives in the
|
|
23
|
+
* meta on SSR pages; the transport middleware never emits a CSP header).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** A sha256 CSP source expression, e.g. `sha256-<base64>`. */
|
|
27
|
+
export type CspHash = `sha256-${string}`
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Compute the CSP sha256 hash of an inline `<script>`/`<style>` body. Hashes the
|
|
31
|
+
* exact bytes the browser hashes — the raw text between the element's open and
|
|
32
|
+
* close tags, with no trimming — so the emitted `'sha256-…'` matches what the
|
|
33
|
+
* browser recomputes for the served content. Mirrors the consent package's
|
|
34
|
+
* `sha256CspHash` (SHA-256 → base64) so the two packages stay wire-compatible.
|
|
35
|
+
*/
|
|
36
|
+
export async function sha256CspHash(content: string): Promise<CspHash> {
|
|
37
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(content))
|
|
38
|
+
const base64 = btoa(String.fromCharCode(...new Uint8Array(digest)))
|
|
39
|
+
return `sha256-${base64}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Inner text of every inline `<script>` (no `src=`) and `<style>` element. */
|
|
43
|
+
interface InlineSources {
|
|
44
|
+
scripts: string[]
|
|
45
|
+
styles: string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const INLINE_SCRIPT_RE = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi
|
|
49
|
+
const INLINE_STYLE_RE = /<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi
|
|
50
|
+
const HAS_SRC_RE = /\bsrc\s*=/i
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract the inner bodies of inline `<script>` and `<style>` elements. A
|
|
54
|
+
* `<script src=…>` reference has no hashable body (its bytes live at another
|
|
55
|
+
* URL, covered by `script-src 'self'`) and is skipped. Empty bodies are skipped
|
|
56
|
+
* — an empty inline script needs no hash. De-duplicates identical bodies so an
|
|
57
|
+
* element repeated on a page contributes a single hash.
|
|
58
|
+
*/
|
|
59
|
+
export function extractInlineSources(html: string): InlineSources {
|
|
60
|
+
const scripts = new Set<string>()
|
|
61
|
+
const styles = new Set<string>()
|
|
62
|
+
|
|
63
|
+
for (const match of html.matchAll(INLINE_SCRIPT_RE)) {
|
|
64
|
+
const attrs = match[1] ?? ''
|
|
65
|
+
const body = match[2] ?? ''
|
|
66
|
+
if (HAS_SRC_RE.test(attrs)) continue
|
|
67
|
+
if (body.length === 0) continue
|
|
68
|
+
scripts.add(body)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const match of html.matchAll(INLINE_STYLE_RE)) {
|
|
72
|
+
const body = match[1] ?? ''
|
|
73
|
+
if (body.length === 0) continue
|
|
74
|
+
styles.add(body)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { scripts: [...scripts], styles: [...styles] }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** True when the document already carries a CSP `<meta http-equiv>` tag. */
|
|
81
|
+
export function hasCspMeta(html: string): boolean {
|
|
82
|
+
return /<meta\b[^>]*\bhttp-equiv\s*=\s*["']?content-security-policy["']?/i.test(html)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build the CSP policy string for an auth page from its inline-source hashes.
|
|
87
|
+
*
|
|
88
|
+
* - `default-src 'self'` is the floor; anything not otherwise named falls here.
|
|
89
|
+
* - `script-src 'self' <hashes>` allows same-origin scripts plus the exact
|
|
90
|
+
* inline scripts on the page. No `'unsafe-inline'`: every inline script is
|
|
91
|
+
* hashed, so the policy stays strict.
|
|
92
|
+
* - `style-src 'self' <hashes>` does the same for inline `<style>` blocks.
|
|
93
|
+
* - `style-src-attr 'unsafe-inline'` permits inline `style="…"` attributes,
|
|
94
|
+
* which are NOT hashable (Astro's own CSP takes the same allowance for
|
|
95
|
+
* attribute styles). Element styles remain hash-pinned.
|
|
96
|
+
* - `img-src 'self' data: https:` covers the brand logo and inline data URIs.
|
|
97
|
+
* - `connect-src 'self'` bounds fetch/XHR (the code-request/verify POSTs are
|
|
98
|
+
* same-origin). `form-action 'self'` bounds the login/verify form posts.
|
|
99
|
+
* - `object-src 'none'`, `base-uri 'self'`, `frame-ancestors 'none'` are the
|
|
100
|
+
* standard hardening trio (no plugins, no base-tag hijack, no framing).
|
|
101
|
+
*/
|
|
102
|
+
export function buildAuthCspPolicy(sources: {
|
|
103
|
+
scriptHashes: CspHash[]
|
|
104
|
+
styleHashes: CspHash[]
|
|
105
|
+
}): string {
|
|
106
|
+
const scriptSrc = ["script-src 'self'", ...sources.scriptHashes.map((h) => `'${h}'`)].join(' ')
|
|
107
|
+
const styleSrc = ["style-src 'self'", ...sources.styleHashes.map((h) => `'${h}'`)].join(' ')
|
|
108
|
+
return [
|
|
109
|
+
"default-src 'self'",
|
|
110
|
+
scriptSrc,
|
|
111
|
+
styleSrc,
|
|
112
|
+
"style-src-attr 'unsafe-inline'",
|
|
113
|
+
"img-src 'self' data: https:",
|
|
114
|
+
"connect-src 'self'",
|
|
115
|
+
"form-action 'self'",
|
|
116
|
+
"object-src 'none'",
|
|
117
|
+
"base-uri 'self'",
|
|
118
|
+
"frame-ancestors 'none'",
|
|
119
|
+
].join('; ')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function escapeHtmlAttribute(value: string): string {
|
|
123
|
+
return value
|
|
124
|
+
.replace(/&/g, '&')
|
|
125
|
+
.replace(/"/g, '"')
|
|
126
|
+
.replace(/'/g, ''')
|
|
127
|
+
.replace(/</g, '<')
|
|
128
|
+
.replace(/>/g, '>')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Insert `tag` immediately after the opening `<head>` (or, absent a head, after
|
|
133
|
+
* `<html>`, or as a prefix) so the policy is parsed before any inline content.
|
|
134
|
+
*/
|
|
135
|
+
function injectIntoHead(html: string, tag: string): string {
|
|
136
|
+
const headOpen = /<head\b[^>]*>/i.exec(html)
|
|
137
|
+
if (headOpen) {
|
|
138
|
+
const at = headOpen.index + headOpen[0].length
|
|
139
|
+
return `${html.slice(0, at)}${tag}${html.slice(at)}`
|
|
140
|
+
}
|
|
141
|
+
const htmlOpen = /<html\b[^>]*>/i.exec(html)
|
|
142
|
+
if (htmlOpen) {
|
|
143
|
+
const at = htmlOpen.index + htmlOpen[0].length
|
|
144
|
+
return `${html.slice(0, at)}${tag}${html.slice(at)}`
|
|
145
|
+
}
|
|
146
|
+
return `${tag}${html}`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Compute the auth CSP for `html` and return the document with a
|
|
151
|
+
* `<meta http-equiv="Content-Security-Policy">` inserted at the top of `<head>`.
|
|
152
|
+
*
|
|
153
|
+
* Idempotent: if the document already carries a CSP meta (a consumer renderer
|
|
154
|
+
* that emits its own), the input is returned unchanged so we never emit two
|
|
155
|
+
* conflicting policies (browsers would intersect them and likely break the
|
|
156
|
+
* page). Every inline `<script>`/`<style>` the page ships is hash-pinned, so the
|
|
157
|
+
* emitted policy is strict — no `'unsafe-inline'` for scripts or element styles.
|
|
158
|
+
*/
|
|
159
|
+
export async function injectAuthCsp(html: string): Promise<string> {
|
|
160
|
+
if (hasCspMeta(html)) return html
|
|
161
|
+
const { scripts, styles } = extractInlineSources(html)
|
|
162
|
+
const scriptHashes = await Promise.all(scripts.map(sha256CspHash))
|
|
163
|
+
const styleHashes = await Promise.all(styles.map(sha256CspHash))
|
|
164
|
+
const policy = buildAuthCspPolicy({ scriptHashes, styleHashes })
|
|
165
|
+
const tag = `<meta http-equiv="Content-Security-Policy" content="${escapeHtmlAttribute(policy)}">`
|
|
166
|
+
return injectIntoHead(html, tag)
|
|
167
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ClientSecretResolutionErrorCode } from './client-secret.js'
|
|
2
|
+
|
|
3
|
+
export type ClientSecretFailureSurface =
|
|
4
|
+
| 'middleware_refresh'
|
|
5
|
+
| 'callback_exchange'
|
|
6
|
+
| 'logout_revocation'
|
|
7
|
+
| 'route_preflight'
|
|
8
|
+
| 'route_exchange'
|
|
9
|
+
| 'server_auth_client'
|
|
10
|
+
|
|
11
|
+
export interface ClientSecretFailureSignal {
|
|
12
|
+
readonly surface: ClientSecretFailureSurface
|
|
13
|
+
readonly code: ClientSecretResolutionErrorCode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Shared no-store headers for all secret-resolution failure responses.
|
|
17
|
+
const SECURITY_HEADERS: Record<string, string> = {
|
|
18
|
+
'Cache-Control': 'private, no-store',
|
|
19
|
+
Pragma: 'no-cache',
|
|
20
|
+
Expires: '0',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function acceptsJson(request: Request | undefined): boolean {
|
|
24
|
+
const accept = request?.headers.get('accept') ?? ''
|
|
25
|
+
return accept
|
|
26
|
+
.split(',')
|
|
27
|
+
.some((value) => value.trim().toLowerCase().split(';')[0] === 'application/json')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Fixed-vocabulary 503 for secret-resolution failures on all surfaces.
|
|
31
|
+
// Browser callers receive bounded plain text; JSON callers receive one fixed
|
|
32
|
+
// JSON error. Neither class reflects a path, provider, error code, raw error,
|
|
33
|
+
// secret value, or stack. Callers must invoke this before issuer I/O.
|
|
34
|
+
export function reportClientSecretResolutionFailure(signal: ClientSecretFailureSignal): void {
|
|
35
|
+
try {
|
|
36
|
+
console.error('[growth-labs/auth] client_secret_resolution_failed', {
|
|
37
|
+
surface: signal.surface,
|
|
38
|
+
code: signal.code,
|
|
39
|
+
})
|
|
40
|
+
} catch {
|
|
41
|
+
// Observability must never replace the bounded fail-closed response.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function secretResolutionFailureResponse(
|
|
46
|
+
request?: Request,
|
|
47
|
+
signal?: ClientSecretFailureSignal,
|
|
48
|
+
): Response {
|
|
49
|
+
if (signal) reportClientSecretResolutionFailure(signal)
|
|
50
|
+
if (acceptsJson(request)) {
|
|
51
|
+
return Response.json(
|
|
52
|
+
{ error: 'auth_service_unavailable' },
|
|
53
|
+
{ status: 503, headers: new Headers(SECURITY_HEADERS as HeadersInit) },
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return new Response('Service unavailable', {
|
|
58
|
+
status: 503,
|
|
59
|
+
headers: new Headers({
|
|
60
|
+
...SECURITY_HEADERS,
|
|
61
|
+
'Content-Type': 'text/plain; charset=UTF-8',
|
|
62
|
+
}),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Typed error thrown by server-helper headersFor() on secret binding failure.
|
|
67
|
+
// Exported so consumers can distinguish secret-resolution failure from
|
|
68
|
+
// network/credential failure in custom server-side auth flows.
|
|
69
|
+
export class ClientSecretResolutionError extends Error {
|
|
70
|
+
override readonly name = 'ClientSecretResolutionError'
|
|
71
|
+
constructor(public readonly code: ClientSecretResolutionErrorCode) {
|
|
72
|
+
super('client_secret_binding_unavailable')
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -10,6 +10,21 @@ export {
|
|
|
10
10
|
normalizeAuthProvider,
|
|
11
11
|
trackAuthEvent,
|
|
12
12
|
} from './analytics.js'
|
|
13
|
+
export type {
|
|
14
|
+
ClientSecretLiteral,
|
|
15
|
+
ClientSecretResolutionErrorCode,
|
|
16
|
+
ClientSecretResult,
|
|
17
|
+
} from './client-secret.js'
|
|
18
|
+
export { isClientSecretLiteral, literalClientSecret } from './client-secret.js'
|
|
19
|
+
export {
|
|
20
|
+
buildAuthCspPolicy,
|
|
21
|
+
type CspHash,
|
|
22
|
+
extractInlineSources,
|
|
23
|
+
hasCspMeta,
|
|
24
|
+
injectAuthCsp,
|
|
25
|
+
sha256CspHash,
|
|
26
|
+
} from './csp.js'
|
|
27
|
+
export { ClientSecretResolutionError } from './fail-closed.js'
|
|
13
28
|
export {
|
|
14
29
|
type BridgeMigrationOptions,
|
|
15
30
|
type BridgeMigrationStatement,
|
|
@@ -25,7 +40,13 @@ export {
|
|
|
25
40
|
rewriteLegacyIdentitySubjects,
|
|
26
41
|
} from './identity.js'
|
|
27
42
|
export { signIdentityCookie, verifyIdentityCookie } from './identity-cookie.js'
|
|
28
|
-
export {
|
|
43
|
+
export {
|
|
44
|
+
EXCHANGE_SECRET_FAILURE,
|
|
45
|
+
type ExchangeSecretFailure,
|
|
46
|
+
getAuthorizationUrl,
|
|
47
|
+
getMagicLinkUrl,
|
|
48
|
+
getPasswordLoginUrl,
|
|
49
|
+
} from './openauth.js'
|
|
29
50
|
export { createPKCEChallenge, generatePKCE } from './pkce.js'
|
|
30
51
|
export { generateState, verifyState } from './state.js'
|
|
31
52
|
export type { VerifiedClaims } from './tokens.js'
|
package/src/utils/openauth.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { getConfig } from '../config-store.js'
|
|
2
2
|
import type { TokenExchangeResult } from '../types.js'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type ClientSecretResolutionErrorCode,
|
|
5
|
+
createClientSecretBasicAuthHeader,
|
|
6
|
+
resolveClientSecret,
|
|
7
|
+
} from './client-secret.js'
|
|
4
8
|
import { resolveInternalIssuer } from './issuer.js'
|
|
5
9
|
import { generatePKCE } from './pkce.js'
|
|
6
10
|
import { generateState } from './state.js'
|
|
@@ -86,22 +90,42 @@ function tokenResponseHasTokens(
|
|
|
86
90
|
)
|
|
87
91
|
}
|
|
88
92
|
|
|
93
|
+
// Bounded sentinel returned when exchangeCode cannot resolve the client secret.
|
|
94
|
+
// Callers check `'secretFailed' in result` to distinguish this from an
|
|
95
|
+
// ordinary token-exchange failure (null) and route to a fail-closed 503
|
|
96
|
+
// rather than a generic auth_failed redirect.
|
|
97
|
+
export interface ExchangeSecretFailure {
|
|
98
|
+
readonly secretFailed: true
|
|
99
|
+
readonly code: ClientSecretResolutionErrorCode
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Backward-compatible exported sentinel for callers that only need a bounded
|
|
103
|
+
// representative failure. Runtime exchangeCode results carry the actual code.
|
|
104
|
+
export const EXCHANGE_SECRET_FAILURE: ExchangeSecretFailure = Object.freeze({
|
|
105
|
+
secretFailed: true,
|
|
106
|
+
code: 'BINDING_REJECTED',
|
|
107
|
+
})
|
|
108
|
+
|
|
89
109
|
export async function exchangeCode(
|
|
90
110
|
callbackUrl: URL,
|
|
91
111
|
code: string,
|
|
92
112
|
codeVerifier: string,
|
|
93
113
|
providerFromCookie?: TokenExchangeResult['provider'],
|
|
94
|
-
|
|
114
|
+
request?: Request,
|
|
115
|
+
): Promise<TokenExchangeResult | ExchangeSecretFailure | null> {
|
|
95
116
|
const options = getConfig()
|
|
96
117
|
const issuer = resolveInternalIssuer(options)
|
|
97
118
|
const clientId = options.clientId || callbackUrl.hostname
|
|
98
119
|
const redirectUri = new URL(options.callbackPath, callbackUrl.origin).toString()
|
|
99
|
-
const
|
|
120
|
+
const secretResult = await resolveClientSecret(options.clientSecret, request)
|
|
121
|
+
if (!secretResult.ok) {
|
|
122
|
+
return Object.freeze({ secretFailed: true as const, code: secretResult.code })
|
|
123
|
+
}
|
|
100
124
|
const headers: Record<string, string> = {
|
|
101
125
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
102
126
|
}
|
|
103
|
-
if (
|
|
104
|
-
headers.Authorization = createClientSecretBasicAuthHeader(clientId,
|
|
127
|
+
if (secretResult.secret) {
|
|
128
|
+
headers.Authorization = createClientSecretBasicAuthHeader(clientId, secretResult.secret)
|
|
105
129
|
}
|
|
106
130
|
const body = new URLSearchParams({
|
|
107
131
|
grant_type: 'authorization_code',
|