@growth-labs/auth 0.17.1 → 0.19.0
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/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +15 -3
- package/dist/middleware/auth.js.map +1 -1
- package/dist/options.d.ts +27 -79
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +11 -1
- package/dist/options.js.map +1 -1
- package/dist/routes/api/code-request.d.ts.map +1 -1
- package/dist/routes/api/code-request.js +14 -2
- 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 +19 -2
- package/dist/routes/api/code-verify.js.map +1 -1
- package/dist/routes/api/rate-limit.d.ts +4 -0
- package/dist/routes/api/rate-limit.d.ts.map +1 -0
- package/dist/routes/api/rate-limit.js +30 -0
- package/dist/routes/api/rate-limit.js.map +1 -0
- package/dist/routes/api/shared.d.ts.map +1 -1
- package/dist/routes/api/shared.js +3 -2
- package/dist/routes/api/shared.js.map +1 -1
- package/dist/routes/callback.d.ts.map +1 -1
- package/dist/routes/callback.js +2 -1
- package/dist/routes/callback.js.map +1 -1
- package/dist/routes/flow.d.ts +1 -1
- package/dist/routes/flow.d.ts.map +1 -1
- package/dist/routes/flow.js +2 -1
- package/dist/routes/flow.js.map +1 -1
- package/dist/routes/login.d.ts.map +1 -1
- package/dist/routes/login.js +6 -0
- package/dist/routes/login.js.map +1 -1
- package/dist/routes/verify-code.d.ts.map +1 -1
- package/dist/routes/verify-code.js +5 -2
- package/dist/routes/verify-code.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -1
- package/dist/server.js.map +1 -1
- package/dist/utils/cookies.d.ts +1 -0
- package/dist/utils/cookies.d.ts.map +1 -1
- package/dist/utils/cookies.js +1 -1
- package/dist/utils/cookies.js.map +1 -1
- package/dist/utils/issuer-fetch.d.ts +5 -0
- package/dist/utils/issuer-fetch.d.ts.map +1 -0
- package/dist/utils/issuer-fetch.js +17 -0
- package/dist/utils/issuer-fetch.js.map +1 -0
- package/dist/utils/issuer.d.ts +2 -0
- package/dist/utils/issuer.d.ts.map +1 -1
- package/dist/utils/issuer.js +12 -0
- package/dist/utils/issuer.js.map +1 -1
- package/dist/utils/openauth.d.ts.map +1 -1
- package/dist/utils/openauth.js +4 -3
- package/dist/utils/openauth.js.map +1 -1
- package/dist/utils/tokens.d.ts +5 -2
- package/dist/utils/tokens.d.ts.map +1 -1
- package/dist/utils/tokens.js +7 -7
- package/dist/utils/tokens.js.map +1 -1
- package/package.json +2 -2
- package/src/middleware/auth.ts +19 -3
- package/src/options.ts +11 -1
- package/src/routes/api/code-request.ts +20 -2
- package/src/routes/api/code-verify.ts +19 -2
- package/src/routes/api/rate-limit.ts +48 -0
- package/src/routes/api/shared.ts +3 -2
- package/src/routes/callback.ts +2 -1
- package/src/routes/flow.ts +2 -0
- package/src/routes/login.ts +6 -0
- package/src/routes/verify-code.ts +8 -2
- package/src/server.ts +11 -5
- package/src/utils/cookies.ts +2 -2
- package/src/utils/issuer-fetch.ts +20 -0
- package/src/utils/issuer.ts +11 -0
- package/src/utils/openauth.ts +9 -3
- package/src/utils/tokens.ts +22 -4
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { getCookie } from '../../utils/cookies.js'
|
|
2
|
+
import { sameOriginRedirect } from '../../utils/issuer.js'
|
|
3
|
+
import { withAuthFlowCookies } from '../flow.js'
|
|
4
|
+
import { enforceCodeRateLimit } from './rate-limit.js'
|
|
1
5
|
import {
|
|
2
6
|
authClient,
|
|
3
7
|
failClosedOnSecretResolutionFailure,
|
|
@@ -25,7 +29,12 @@ export async function POST(context: RouteContext): Promise<Response> {
|
|
|
25
29
|
|
|
26
30
|
const form = await readForm(context)
|
|
27
31
|
const email = await readFormString(form, 'email')
|
|
28
|
-
const redirect =
|
|
32
|
+
const redirect =
|
|
33
|
+
form.get('redirect')?.toString() ??
|
|
34
|
+
getCookie(context.request, `${options.cookiePrefix}_redirect`) ??
|
|
35
|
+
undefined
|
|
36
|
+
const limited = await enforceCodeRateLimit(context, 'codeRequest', email)
|
|
37
|
+
if (limited) return limited
|
|
29
38
|
const surface =
|
|
30
39
|
form.get('auth_surface')?.toString() === 'create-account' ? 'create-account' : 'login'
|
|
31
40
|
const surfacePath = surface === 'create-account' ? '/create-account/' : options.verifyCodePath
|
|
@@ -39,7 +48,7 @@ export async function POST(context: RouteContext): Promise<Response> {
|
|
|
39
48
|
})
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
|
|
51
|
+
const response = statusRedirect(
|
|
43
52
|
context,
|
|
44
53
|
result,
|
|
45
54
|
surfacePath,
|
|
@@ -48,4 +57,13 @@ export async function POST(context: RouteContext): Promise<Response> {
|
|
|
48
57
|
{ email, redirect },
|
|
49
58
|
'email-code',
|
|
50
59
|
)
|
|
60
|
+
if ('error' in result) return response
|
|
61
|
+
return withAuthFlowCookies(
|
|
62
|
+
context,
|
|
63
|
+
options,
|
|
64
|
+
response,
|
|
65
|
+
sameOriginRedirect(redirect, context.url.origin),
|
|
66
|
+
'email-code',
|
|
67
|
+
900,
|
|
68
|
+
)
|
|
51
69
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { clearAuthFlowCookies, getCookie } from '../../utils/cookies.js'
|
|
2
|
+
import { enforceCodeRateLimit } from './rate-limit.js'
|
|
1
3
|
import {
|
|
2
4
|
authClient,
|
|
3
5
|
callbackRedirect,
|
|
@@ -25,20 +27,35 @@ export async function POST(context: RouteContext): Promise<Response> {
|
|
|
25
27
|
const form = await readForm(context)
|
|
26
28
|
const email = await readFormString(form, 'email')
|
|
27
29
|
const code = await readFormString(form, 'code')
|
|
30
|
+
const limited = await enforceCodeRateLimit(context, 'codeVerify', email)
|
|
31
|
+
if (limited) {
|
|
32
|
+
clearAuthFlowCookies(limited, options)
|
|
33
|
+
return limited
|
|
34
|
+
}
|
|
28
35
|
const errorPath =
|
|
29
36
|
form.get('auth_surface')?.toString() === 'create-account'
|
|
30
37
|
? '/create-account/'
|
|
31
38
|
: options.verifyCodePath
|
|
32
39
|
const result = await authClient(context).code.verify({ email, code })
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
const response = await callbackRedirect(
|
|
35
42
|
context,
|
|
36
43
|
result,
|
|
37
44
|
errorPath,
|
|
38
45
|
{
|
|
39
46
|
email,
|
|
40
|
-
redirect:
|
|
47
|
+
redirect:
|
|
48
|
+
form.get('redirect')?.toString() ??
|
|
49
|
+
getCookie(context.request, `${options.cookiePrefix}_redirect`) ??
|
|
50
|
+
undefined,
|
|
41
51
|
},
|
|
42
52
|
'email-code',
|
|
43
53
|
)
|
|
54
|
+
if (!('error' in result)) return response
|
|
55
|
+
const mutable = new Response(response.body, {
|
|
56
|
+
status: response.status,
|
|
57
|
+
headers: new Headers(response.headers),
|
|
58
|
+
})
|
|
59
|
+
clearAuthFlowCookies(mutable, options)
|
|
60
|
+
return mutable
|
|
44
61
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getConfig } from '../../config-store.js'
|
|
2
|
+
import { getRuntimeEnv } from '../../runtime.js'
|
|
3
|
+
import type { RouteContext } from '../flow.js'
|
|
4
|
+
|
|
5
|
+
interface Limiter {
|
|
6
|
+
limit(input: { key: string }): Promise<{ success: boolean }>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Both dimensions use native rate limits; email keys contain only a SHA-256 digest. */
|
|
10
|
+
export async function enforceCodeRateLimit(
|
|
11
|
+
context: RouteContext,
|
|
12
|
+
operation: 'codeRequest' | 'codeVerify',
|
|
13
|
+
email: string,
|
|
14
|
+
): Promise<Response | null> {
|
|
15
|
+
const bindings = getConfig().rateLimits?.[operation]
|
|
16
|
+
if (!bindings) return null
|
|
17
|
+
const env = getRuntimeEnv()
|
|
18
|
+
try {
|
|
19
|
+
const digest = await crypto.subtle.digest(
|
|
20
|
+
'SHA-256',
|
|
21
|
+
new TextEncoder().encode(email.trim().toLowerCase()),
|
|
22
|
+
)
|
|
23
|
+
const emailKey = Array.from(new Uint8Array(digest), (byte) =>
|
|
24
|
+
byte.toString(16).padStart(2, '0'),
|
|
25
|
+
).join('')
|
|
26
|
+
for (const [dimension, key] of [
|
|
27
|
+
['ip', context.request.headers.get('cf-connecting-ip') || 'unknown'],
|
|
28
|
+
['email', emailKey],
|
|
29
|
+
] as const) {
|
|
30
|
+
const binding = env[bindings[dimension]] as Limiter | undefined
|
|
31
|
+
if (!binding || typeof binding.limit !== 'function')
|
|
32
|
+
throw new Error('missing rate limit binding')
|
|
33
|
+
const result = await binding.limit({ key: `${operation}:${dimension}:${key}` })
|
|
34
|
+
if (!result.success)
|
|
35
|
+
return Response.json(
|
|
36
|
+
{ error: 'rate_limited' },
|
|
37
|
+
{ status: 429, headers: { 'Cache-Control': 'private, no-store', 'Retry-After': '60' } },
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
return null
|
|
41
|
+
} catch {
|
|
42
|
+
console.error('@growth-labs/auth: configured code rate limiter unavailable')
|
|
43
|
+
return Response.json(
|
|
44
|
+
{ error: 'auth_service_unavailable' },
|
|
45
|
+
{ status: 503, headers: { 'Cache-Control': 'private, no-store' } },
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/routes/api/shared.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { secretResolutionFailureResponse } from '../../utils/fail-closed.js'
|
|
20
20
|
import { canonicalIdentitySubject } from '../../utils/identity.js'
|
|
21
21
|
import { createIdentityCookiePayload, signIdentityCookie } from '../../utils/identity-cookie.js'
|
|
22
|
+
import { sameOriginRedirect } from '../../utils/issuer.js'
|
|
22
23
|
import { exchangeCode } from '../../utils/openauth.js'
|
|
23
24
|
import { resolveWaitUntil } from '../../utils/wait-until.js'
|
|
24
25
|
import { type RouteContext, readForm, redirectLocation, redirectTo } from '../flow.js'
|
|
@@ -132,7 +133,7 @@ function routeResult(
|
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
return Response.json(error ? { error, redirect: location } : { ok: true, redirect: location }, {
|
|
135
|
-
status: error ? 400 : 200,
|
|
136
|
+
status: error === 'rate_limited' || error === 'too_many_attempts' ? 429 : error ? 400 : 200,
|
|
136
137
|
})
|
|
137
138
|
}
|
|
138
139
|
|
|
@@ -202,7 +203,7 @@ async function completeJsonAuth(
|
|
|
202
203
|
const tokenResponse = exchangeResult
|
|
203
204
|
|
|
204
205
|
const redirectPath = getCookie(context.request, `${options.cookiePrefix}_redirect`) || '/'
|
|
205
|
-
const redirect =
|
|
206
|
+
const redirect = sameOriginRedirect(redirectPath, context.url.origin)
|
|
206
207
|
const response = mutableHeaders(Response.json({ ok: true, redirect }))
|
|
207
208
|
|
|
208
209
|
setTokenCookies(response, options, tokenResponse.accessToken, tokenResponse.refreshToken)
|
package/src/routes/callback.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { secretResolutionFailureResponse } from '../utils/fail-closed.js'
|
|
15
15
|
import { canonicalIdentitySubject } from '../utils/identity.js'
|
|
16
16
|
import { createIdentityCookiePayload, signIdentityCookie } from '../utils/identity-cookie.js'
|
|
17
|
+
import { sameOriginRedirect } from '../utils/issuer.js'
|
|
17
18
|
import { exchangeCode } from '../utils/openauth.js'
|
|
18
19
|
import { verifyState } from '../utils/state.js'
|
|
19
20
|
import { resolveWaitUntil } from '../utils/wait-until.js'
|
|
@@ -131,7 +132,7 @@ export async function GET(context: CallbackContext): Promise<Response> {
|
|
|
131
132
|
|
|
132
133
|
// Build redirect response
|
|
133
134
|
const redirectPath = getCookie(request, `${options.cookiePrefix}_redirect`) || '/'
|
|
134
|
-
const redirect =
|
|
135
|
+
const redirect = sameOriginRedirect(redirectPath, url.origin)
|
|
135
136
|
const response = wantsJson(request)
|
|
136
137
|
? Response.json({ ok: true, redirect })
|
|
137
138
|
: Response.redirect(redirect, 302)
|
package/src/routes/flow.ts
CHANGED
|
@@ -19,6 +19,7 @@ export async function withAuthFlowCookies(
|
|
|
19
19
|
response: Response,
|
|
20
20
|
redirectPath: string,
|
|
21
21
|
provider?: string,
|
|
22
|
+
maxAge?: number,
|
|
22
23
|
): Promise<Response> {
|
|
23
24
|
const authFlow = await createAuthFlow(context)
|
|
24
25
|
const mutable = new Response(response.body, {
|
|
@@ -29,6 +30,7 @@ export async function withAuthFlowCookies(
|
|
|
29
30
|
setAuthFlowCookies(mutable, options, authFlow.verifier, authFlow.state, redirectPath, provider, {
|
|
30
31
|
pkcePath: '/',
|
|
31
32
|
additionalPkcePaths: [options.callbackPath],
|
|
33
|
+
maxAge,
|
|
32
34
|
})
|
|
33
35
|
return mutable
|
|
34
36
|
}
|
package/src/routes/login.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { loginPageScript } from '../client/login-page.js'
|
|
|
2
2
|
import { getConfig } from '../config-store.js'
|
|
3
3
|
import { getRenderer, loginRendererPayload, renderHtmlResponse } from '../renderers.js'
|
|
4
4
|
import { setAuthFlowCookies } from '../utils/cookies.js'
|
|
5
|
+
import { sameOriginRedirect } from '../utils/issuer.js'
|
|
5
6
|
import { createAuthFlow, getAuthorizationUrl } from '../utils/openauth.js'
|
|
6
7
|
|
|
7
8
|
interface LoginContext {
|
|
@@ -24,6 +25,11 @@ export async function GET(context: LoginContext): Promise<Response> {
|
|
|
24
25
|
selectedProvider &&
|
|
25
26
|
options.providers.includes(selectedProvider as (typeof options.providers)[number])
|
|
26
27
|
) {
|
|
28
|
+
if (selectedProvider === 'email-code' && getRenderer('verifyCode')) {
|
|
29
|
+
const local = new URL(options.verifyCodePath, context.url.origin)
|
|
30
|
+
local.searchParams.set('redirect', sameOriginRedirect(redirectPath, context.url.origin))
|
|
31
|
+
return Response.redirect(local, 302)
|
|
32
|
+
}
|
|
27
33
|
const authFlow = await createAuthFlow(context)
|
|
28
34
|
const issuerProvider = selectedProvider === 'email-code' ? 'code' : selectedProvider
|
|
29
35
|
const { url } = await getAuthorizationUrl(context, issuerProvider, redirectPath, authFlow)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getConfig } from '../config-store.js'
|
|
2
2
|
import { getRenderer, renderHtmlResponse, verifyCodeRendererPayload } from '../renderers.js'
|
|
3
|
+
import { getCookie } from '../utils/cookies.js'
|
|
4
|
+
import { sameOriginRedirect } from '../utils/issuer.js'
|
|
3
5
|
import { type RouteContext, withAuthFlowCookies } from './flow.js'
|
|
4
6
|
|
|
5
7
|
export async function GET(context: RouteContext): Promise<Response> {
|
|
@@ -13,7 +15,11 @@ export async function GET(context: RouteContext): Promise<Response> {
|
|
|
13
15
|
return new Response('Not found', { status: 404 })
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
const redirectPath =
|
|
18
|
+
const redirectPath = sameOriginRedirect(
|
|
19
|
+
context.url.searchParams.get('redirect') ??
|
|
20
|
+
getCookie(context.request, `${options.cookiePrefix}_redirect`),
|
|
21
|
+
context.url.origin,
|
|
22
|
+
)
|
|
17
23
|
const authSurface =
|
|
18
24
|
context.url.pathname === '/create-account/' ||
|
|
19
25
|
context.url.searchParams.get('auth_surface') === 'create-account'
|
|
@@ -30,5 +36,5 @@ export async function GET(context: RouteContext): Promise<Response> {
|
|
|
30
36
|
}),
|
|
31
37
|
),
|
|
32
38
|
)
|
|
33
|
-
return withAuthFlowCookies(context, options, response, redirectPath, 'email-code')
|
|
39
|
+
return withAuthFlowCookies(context, options, response, redirectPath, 'email-code', 900)
|
|
34
40
|
}
|
package/src/server.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
reportClientSecretResolutionFailure,
|
|
8
8
|
} from './utils/fail-closed.js'
|
|
9
9
|
import { resolveInternalIssuer } from './utils/issuer.js'
|
|
10
|
+
import { resolveIssuerFetch } from './utils/issuer-fetch.js'
|
|
10
11
|
import { createPKCEChallenge } from './utils/pkce.js'
|
|
11
12
|
|
|
12
13
|
export { ClientSecretResolutionError }
|
|
@@ -75,6 +76,8 @@ async function headersFor(
|
|
|
75
76
|
context: AuthClientContext,
|
|
76
77
|
): Promise<Record<string, string>> {
|
|
77
78
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
|
79
|
+
const clientIp = context.request.headers.get('cf-connecting-ip')
|
|
80
|
+
if (clientIp) headers['X-Auth-Client-IP'] = clientIp
|
|
78
81
|
const secretResult = await resolveClientSecret(options.clientSecret, context.request)
|
|
79
82
|
if (!secretResult.ok) throw new ClientSecretResolutionError(secretResult.code)
|
|
80
83
|
if (secretResult.secret) {
|
|
@@ -106,11 +109,14 @@ async function postJson<T extends AuthClientCodeResult | AuthClientOkResult>(
|
|
|
106
109
|
body: Record<string, unknown>,
|
|
107
110
|
): Promise<T> {
|
|
108
111
|
try {
|
|
109
|
-
const response = await
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
const response = await resolveIssuerFetch(options)(
|
|
113
|
+
`${resolveInternalIssuer(options)}${pathname}`,
|
|
114
|
+
{
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: await headersFor(options, context),
|
|
117
|
+
body: JSON.stringify(body),
|
|
118
|
+
},
|
|
119
|
+
)
|
|
114
120
|
const json = await readJson(response)
|
|
115
121
|
|
|
116
122
|
if (typeof json.error === 'string') {
|
package/src/utils/cookies.ts
CHANGED
|
@@ -112,14 +112,14 @@ export function setAuthFlowCookies(
|
|
|
112
112
|
state: string,
|
|
113
113
|
redirectPath: string,
|
|
114
114
|
provider?: string,
|
|
115
|
-
flowOptions: { pkcePath?: string; additionalPkcePaths?: string[] } = {},
|
|
115
|
+
flowOptions: { pkcePath?: string; additionalPkcePaths?: string[]; maxAge?: number } = {},
|
|
116
116
|
): void {
|
|
117
117
|
const baseAttrs: CookieAttributes = {
|
|
118
118
|
path: '/',
|
|
119
119
|
httpOnly: true,
|
|
120
120
|
secure: true,
|
|
121
121
|
sameSite: 'lax',
|
|
122
|
-
maxAge: 600,
|
|
122
|
+
maxAge: flowOptions.maxAge ?? 600,
|
|
123
123
|
}
|
|
124
124
|
const pkcePaths = new Set([
|
|
125
125
|
flowOptions.pkcePath ?? options.callbackPath,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ResolvedAuthOptions } from '../options.js'
|
|
2
|
+
import { getRuntimeEnv } from '../runtime.js'
|
|
3
|
+
import type { IssuerFetch } from './tokens.js'
|
|
4
|
+
|
|
5
|
+
/** Resolve lazily, retaining the binding receiver and never falling back on a bad binding. */
|
|
6
|
+
export function resolveIssuerFetch(options: Pick<ResolvedAuthOptions, 'issuerFetch'>): IssuerFetch {
|
|
7
|
+
if (!options.issuerFetch) return fetch
|
|
8
|
+
return (input, init) => {
|
|
9
|
+
const binding = getRuntimeEnv()[options.issuerFetch!]
|
|
10
|
+
if (
|
|
11
|
+
typeof binding !== 'object' ||
|
|
12
|
+
binding === null ||
|
|
13
|
+
!('fetch' in binding) ||
|
|
14
|
+
typeof binding.fetch !== 'function'
|
|
15
|
+
) {
|
|
16
|
+
throw new Error('@growth-labs/auth: configured issuer service binding unavailable')
|
|
17
|
+
}
|
|
18
|
+
return binding.fetch(input, init)
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/utils/issuer.ts
CHANGED
|
@@ -5,3 +5,14 @@ export function resolveInternalIssuer(
|
|
|
5
5
|
): string {
|
|
6
6
|
return options.issuerInternal ?? options.issuer
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
/** Redirect cookies and form return paths are untrusted input. */
|
|
10
|
+
export function sameOriginRedirect(value: string | null | undefined, origin: string): string {
|
|
11
|
+
try {
|
|
12
|
+
const target = new URL(value || '/', origin)
|
|
13
|
+
if (target.origin === origin && !target.username && !target.password) return target.toString()
|
|
14
|
+
} catch {
|
|
15
|
+
/* use the local home page */
|
|
16
|
+
}
|
|
17
|
+
return new URL('/', origin).toString()
|
|
18
|
+
}
|
package/src/utils/openauth.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
resolveClientSecret,
|
|
7
7
|
} from './client-secret.js'
|
|
8
8
|
import { resolveInternalIssuer } from './issuer.js'
|
|
9
|
+
import { resolveIssuerFetch } from './issuer-fetch.js'
|
|
9
10
|
import { generatePKCE } from './pkce.js'
|
|
10
11
|
import { generateState } from './state.js'
|
|
11
12
|
import { verifyIssuedAccessTokenClaims } from './tokens.js'
|
|
@@ -137,7 +138,7 @@ export async function exchangeCode(
|
|
|
137
138
|
})
|
|
138
139
|
|
|
139
140
|
try {
|
|
140
|
-
const response = await
|
|
141
|
+
const response = await resolveIssuerFetch(options)(`${issuer}/token`, {
|
|
141
142
|
method: 'POST',
|
|
142
143
|
headers,
|
|
143
144
|
body: body.toString(),
|
|
@@ -148,7 +149,12 @@ export async function exchangeCode(
|
|
|
148
149
|
const data = await response.json()
|
|
149
150
|
if (!tokenResponseHasTokens(data)) return null
|
|
150
151
|
|
|
151
|
-
const claims = await verifyIssuedAccessTokenClaims(
|
|
152
|
+
const claims = await verifyIssuedAccessTokenClaims(
|
|
153
|
+
options.issuerFetch ? options.issuer : issuer,
|
|
154
|
+
data.access_token,
|
|
155
|
+
options.resource,
|
|
156
|
+
{ jwksIssuer: issuer, jwksFetch: resolveIssuerFetch(options) },
|
|
157
|
+
)
|
|
152
158
|
if (!claims) return null
|
|
153
159
|
|
|
154
160
|
return {
|
|
@@ -181,7 +187,7 @@ export async function revokeToken(
|
|
|
181
187
|
}
|
|
182
188
|
|
|
183
189
|
try {
|
|
184
|
-
await
|
|
190
|
+
await resolveIssuerFetch(options)(`${issuer}/revoke`, {
|
|
185
191
|
method: 'POST',
|
|
186
192
|
headers,
|
|
187
193
|
body: new URLSearchParams({
|
package/src/utils/tokens.ts
CHANGED
|
@@ -216,9 +216,10 @@ export async function verifyIssuedAccessTokenClaims(
|
|
|
216
216
|
issuer: string,
|
|
217
217
|
token: string,
|
|
218
218
|
audience: string | undefined,
|
|
219
|
+
options: Pick<VerifyAccessTokenOptions, 'jwksFetch' | 'jwksIssuer'> = {},
|
|
219
220
|
): Promise<TokenPayloadClaims | null> {
|
|
220
221
|
if (!audience) return null
|
|
221
|
-
const payload = await verifyAccessTokenPayload(issuer, token, { audience })
|
|
222
|
+
const payload = await verifyAccessTokenPayload(issuer, token, { audience, ...options })
|
|
222
223
|
if (!payload) return null
|
|
223
224
|
return extractTokenClaims(payload)
|
|
224
225
|
}
|
|
@@ -298,6 +299,8 @@ async function exchangeRefreshToken(
|
|
|
298
299
|
clientId: string | undefined,
|
|
299
300
|
clientSecret: string | undefined,
|
|
300
301
|
resource: string,
|
|
302
|
+
fetcher: IssuerFetch,
|
|
303
|
+
verificationIssuer: string,
|
|
301
304
|
): Promise<RefreshExchange> {
|
|
302
305
|
try {
|
|
303
306
|
const params = new URLSearchParams({
|
|
@@ -314,7 +317,7 @@ async function exchangeRefreshToken(
|
|
|
314
317
|
headers.Authorization = createClientSecretBasicAuthHeader(clientId, clientSecret)
|
|
315
318
|
}
|
|
316
319
|
|
|
317
|
-
const response = await
|
|
320
|
+
const response = await fetcher(`${issuer}/token`, {
|
|
318
321
|
method: 'POST',
|
|
319
322
|
headers,
|
|
320
323
|
body: params.toString(),
|
|
@@ -339,7 +342,12 @@ async function exchangeRefreshToken(
|
|
|
339
342
|
return { result: null, failure: { reason: 'malformed_response', status: response.status } }
|
|
340
343
|
}
|
|
341
344
|
|
|
342
|
-
const claims = await verifyIssuedAccessTokenClaims(
|
|
345
|
+
const claims = await verifyIssuedAccessTokenClaims(
|
|
346
|
+
verificationIssuer,
|
|
347
|
+
data.access_token,
|
|
348
|
+
resource,
|
|
349
|
+
{ jwksIssuer: issuer, jwksFetch: fetcher },
|
|
350
|
+
)
|
|
343
351
|
if (!claims) {
|
|
344
352
|
return { result: null, failure: { reason: 'claims_rejected', status: response.status } }
|
|
345
353
|
}
|
|
@@ -367,6 +375,7 @@ export async function refreshTokens(
|
|
|
367
375
|
clientSecret?: string,
|
|
368
376
|
resource?: string,
|
|
369
377
|
onFailure?: (failure: RefreshFailure) => void,
|
|
378
|
+
transport: { issuerFetch?: IssuerFetch; verificationIssuer?: string } = {},
|
|
370
379
|
): Promise<TokenRefreshResult | null> {
|
|
371
380
|
// Reporting is advisory: a callback that throws must not change the outcome.
|
|
372
381
|
const report = (failure: RefreshFailure): void => {
|
|
@@ -382,7 +391,16 @@ export async function refreshTokens(
|
|
|
382
391
|
}
|
|
383
392
|
const exchange = await singleFlightRefresh(
|
|
384
393
|
['tokens', issuer, clientId ?? '', resource, refreshToken],
|
|
385
|
-
() =>
|
|
394
|
+
() =>
|
|
395
|
+
exchangeRefreshToken(
|
|
396
|
+
issuer,
|
|
397
|
+
refreshToken,
|
|
398
|
+
clientId,
|
|
399
|
+
clientSecret,
|
|
400
|
+
resource,
|
|
401
|
+
transport.issuerFetch ?? fetch,
|
|
402
|
+
transport.verificationIssuer ?? issuer,
|
|
403
|
+
),
|
|
386
404
|
)
|
|
387
405
|
if (exchange.failure) report(exchange.failure)
|
|
388
406
|
return exchange.result
|