@growth-labs/auth 0.10.4 → 0.12.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/routes/api/code-request.d.ts.map +1 -1
- package/dist/routes/api/code-request.js +10 -4
- package/dist/routes/api/code-request.js.map +1 -1
- package/dist/routes/api/password-reset-request.d.ts.map +1 -1
- package/dist/routes/api/password-reset-request.js +4 -1
- package/dist/routes/api/password-reset-request.js.map +1 -1
- package/dist/routes/api/shared.d.ts +7 -0
- package/dist/routes/api/shared.d.ts.map +1 -1
- package/dist/routes/api/shared.js +15 -1
- package/dist/routes/api/shared.js.map +1 -1
- package/dist/utils/client-secret-header.d.ts +2 -0
- package/dist/utils/client-secret-header.d.ts.map +1 -0
- package/dist/utils/client-secret-header.js +8 -0
- package/dist/utils/client-secret-header.js.map +1 -0
- package/dist/utils/client-secret.d.ts +1 -1
- package/dist/utils/client-secret.d.ts.map +1 -1
- package/dist/utils/client-secret.js +1 -7
- package/dist/utils/client-secret.js.map +1 -1
- package/dist/utils/cookies.d.ts.map +1 -1
- package/dist/utils/cookies.js +8 -1
- package/dist/utils/cookies.js.map +1 -1
- package/dist/utils/tokens.d.ts +9 -0
- package/dist/utils/tokens.d.ts.map +1 -1
- package/dist/utils/tokens.js +33 -2
- package/dist/utils/tokens.js.map +1 -1
- package/dist/workers.d.ts +89 -0
- package/dist/workers.d.ts.map +1 -0
- package/dist/workers.js +516 -0
- package/dist/workers.js.map +1 -0
- package/package.json +22 -3
- package/src/routes/api/code-request.ts +12 -4
- package/src/routes/api/password-reset-request.ts +5 -0
- package/src/routes/api/shared.ts +21 -0
- package/src/utils/client-secret-header.ts +8 -0
- package/src/utils/client-secret.ts +1 -9
- package/src/utils/cookies.ts +6 -1
- package/src/utils/tokens.ts +46 -2
- package/src/workers.ts +758 -0
package/src/routes/api/shared.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type AuthClientCodeResult, type AuthClientOkResult, authClient } from '
|
|
|
4
4
|
import {
|
|
5
5
|
type AuthAnalyticsProvider,
|
|
6
6
|
analyticsAvailable,
|
|
7
|
+
emitAuthEvent,
|
|
7
8
|
emitAuthFailureEvent,
|
|
8
9
|
emitAuthSuccessEvent,
|
|
9
10
|
} from '../../utils/analytics.js'
|
|
@@ -240,6 +241,26 @@ function queueAuthFailure(
|
|
|
240
241
|
)
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Fire-and-forget step event for the auth funnel's intermediate stages
|
|
246
|
+
* (code requested, reset requested). Success/failure terminals already
|
|
247
|
+
* emit via emitAuthSuccessEvent / queueAuthFailure; without the steps
|
|
248
|
+
* between them, a request→verify drop-off is invisible in analytics.
|
|
249
|
+
*/
|
|
250
|
+
export function queueAuthStep(
|
|
251
|
+
context: RouteContext,
|
|
252
|
+
eventName: string,
|
|
253
|
+
data: Record<string, unknown>,
|
|
254
|
+
): void {
|
|
255
|
+
if (!analyticsAvailable(context)) return
|
|
256
|
+
resolveWaitUntil(context)(
|
|
257
|
+
emitAuthEvent(context, eventName, {
|
|
258
|
+
...data,
|
|
259
|
+
authPath: context.url.pathname,
|
|
260
|
+
}),
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
243
264
|
async function setOptionalIdentityCookie(response: Response, subject: string): Promise<void> {
|
|
244
265
|
const secret = getRuntimeEnv().GL_IDENTITY_SECRET
|
|
245
266
|
if (typeof secret !== 'string' || !secret) return
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function createClientSecretBasicAuthHeader(clientId: string, clientSecret: string): string {
|
|
2
|
+
return `Basic ${btoa(`${formEncode(clientId)}:${formEncode(clientSecret)}`)}`
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function formEncode(value: string): string {
|
|
6
|
+
const params = new URLSearchParams({ value })
|
|
7
|
+
return params.toString().slice('value='.length)
|
|
8
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
literalClientSecret,
|
|
6
6
|
} from './client-secret-literal.js'
|
|
7
7
|
|
|
8
|
+
export { createClientSecretBasicAuthHeader } from './client-secret-header.js'
|
|
8
9
|
export type { ClientSecretLiteral }
|
|
9
10
|
export { isClientSecretLiteral, literalClientSecret }
|
|
10
11
|
|
|
@@ -140,12 +141,3 @@ export async function resolveClientSecret(
|
|
|
140
141
|
// No Request context: resolve without caching (test or internal call).
|
|
141
142
|
return doResolve(clientSecret)
|
|
142
143
|
}
|
|
143
|
-
|
|
144
|
-
export function createClientSecretBasicAuthHeader(clientId: string, clientSecret: string): string {
|
|
145
|
-
return `Basic ${btoa(`${formEncode(clientId)}:${formEncode(clientSecret)}`)}`
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function formEncode(value: string): string {
|
|
149
|
-
const params = new URLSearchParams({ value })
|
|
150
|
-
return params.toString().slice('value='.length)
|
|
151
|
-
}
|
package/src/utils/cookies.ts
CHANGED
|
@@ -16,7 +16,12 @@ export function getCookie(request: Request, name: string): string | null {
|
|
|
16
16
|
const header = request.headers.get('cookie')
|
|
17
17
|
if (!header) return null
|
|
18
18
|
const match = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
|
|
19
|
-
|
|
19
|
+
if (!match) return null
|
|
20
|
+
try {
|
|
21
|
+
return decodeURIComponent(match[1])
|
|
22
|
+
} catch {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export function setCookie(
|
package/src/utils/tokens.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createLocalJWKSet, jwtVerify } from 'jose'
|
|
2
2
|
import { JWTExpired } from 'jose/errors'
|
|
3
3
|
import type { TokenRefreshResult } from '../types.js'
|
|
4
|
-
import { createClientSecretBasicAuthHeader } from './client-secret.js'
|
|
4
|
+
import { createClientSecretBasicAuthHeader } from './client-secret-header.js'
|
|
5
|
+
import { extractIdentityId } from './identity.js'
|
|
5
6
|
|
|
6
7
|
type JWKSFunction = ReturnType<typeof createLocalJWKSet>
|
|
7
8
|
|
|
@@ -69,6 +70,9 @@ export interface VerifiedClaims {
|
|
|
69
70
|
image?: string
|
|
70
71
|
synthetic?: boolean
|
|
71
72
|
purpose?: string
|
|
73
|
+
userId?: string
|
|
74
|
+
roles?: string[]
|
|
75
|
+
audiences?: string[]
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
export interface TokenPayloadClaims extends VerifiedClaims {
|
|
@@ -103,6 +107,12 @@ export interface VerifyAccessTokenOptions {
|
|
|
103
107
|
audience?: string | string[]
|
|
104
108
|
/** Signature algorithms to accept. Defaults to `['ES256']` (identity-platform's signing alg). */
|
|
105
109
|
algorithms?: string[]
|
|
110
|
+
/** Fetch signing keys from this backchannel base while still enforcing `issuer`. */
|
|
111
|
+
jwksIssuer?: string
|
|
112
|
+
/** Enforce the identity-platform user access-token claim contract. */
|
|
113
|
+
requireUserToken?: boolean
|
|
114
|
+
/** Permitted JWT clock skew in seconds for strict user-token validation. */
|
|
115
|
+
clockSkew?: number
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
async function verifyAccessTokenPayload(
|
|
@@ -113,15 +123,38 @@ async function verifyAccessTokenPayload(
|
|
|
113
123
|
try {
|
|
114
124
|
const header = decodeJwtHeader(token)
|
|
115
125
|
const kid = typeof header.kid === 'string' ? header.kid : undefined
|
|
116
|
-
const jwks = await getJWKS(issuer, kid)
|
|
126
|
+
const jwks = await getJWKS(options.jwksIssuer ?? issuer, kid)
|
|
117
127
|
const { payload } = await jwtVerify(token, jwks, {
|
|
118
128
|
issuer,
|
|
119
129
|
audience: options.audience,
|
|
120
130
|
algorithms: options.algorithms ?? ['ES256'],
|
|
131
|
+
clockTolerance: options.clockSkew,
|
|
121
132
|
})
|
|
122
133
|
if (payload.mode !== 'access') {
|
|
123
134
|
throw new Error(`unexpected token mode: ${String(payload.mode)}`)
|
|
124
135
|
}
|
|
136
|
+
if (options.requireUserToken) {
|
|
137
|
+
const properties = payload.properties
|
|
138
|
+
if (
|
|
139
|
+
payload.type !== 'user' ||
|
|
140
|
+
typeof payload.iat !== 'number' ||
|
|
141
|
+
typeof payload.exp !== 'number' ||
|
|
142
|
+
typeof payload.sub !== 'string' ||
|
|
143
|
+
typeof properties !== 'object' ||
|
|
144
|
+
properties === null
|
|
145
|
+
) {
|
|
146
|
+
throw new Error('required user access-token claims are missing')
|
|
147
|
+
}
|
|
148
|
+
const userId = extractIdentityId(payload.sub)
|
|
149
|
+
if (
|
|
150
|
+
payload.iat > Math.floor(Date.now() / 1000) + (options.clockSkew ?? 0) ||
|
|
151
|
+
(properties as Record<string, unknown>).userId !== userId ||
|
|
152
|
+
typeof (properties as Record<string, unknown>).email !== 'string' ||
|
|
153
|
+
(properties as Record<string, unknown>).email === ''
|
|
154
|
+
) {
|
|
155
|
+
throw new Error('access-token identity claims are inconsistent')
|
|
156
|
+
}
|
|
157
|
+
}
|
|
125
158
|
return payload as Record<string, unknown>
|
|
126
159
|
} catch (err) {
|
|
127
160
|
if (!isExpiredJwtError(err)) {
|
|
@@ -139,6 +172,12 @@ export async function verifyAccessToken(
|
|
|
139
172
|
const payload = await verifyAccessTokenPayload(issuer, token, options)
|
|
140
173
|
if (!payload) return null
|
|
141
174
|
const claims = extractTokenClaims(payload)
|
|
175
|
+
const properties = (payload.properties ?? {}) as Record<string, unknown>
|
|
176
|
+
const audience = Array.isArray(payload.aud)
|
|
177
|
+
? payload.aud.filter((value): value is string => typeof value === 'string')
|
|
178
|
+
: typeof payload.aud === 'string'
|
|
179
|
+
? [payload.aud]
|
|
180
|
+
: []
|
|
142
181
|
return {
|
|
143
182
|
sub: claims.sub,
|
|
144
183
|
email: claims.email,
|
|
@@ -146,6 +185,11 @@ export async function verifyAccessToken(
|
|
|
146
185
|
image: claims.image,
|
|
147
186
|
synthetic: payload.synthetic === true ? true : undefined,
|
|
148
187
|
purpose: typeof payload.purpose === 'string' ? payload.purpose : undefined,
|
|
188
|
+
userId: typeof properties.userId === 'string' ? properties.userId : undefined,
|
|
189
|
+
roles: Array.isArray(payload.roles)
|
|
190
|
+
? payload.roles.filter((value): value is string => typeof value === 'string')
|
|
191
|
+
: undefined,
|
|
192
|
+
audiences: audience,
|
|
149
193
|
}
|
|
150
194
|
}
|
|
151
195
|
|