@o/better-auth-utils 0.16.19
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/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/cjs/createAuthClient.cjs +251 -0
- package/dist/cjs/createAuthClient.native.js +279 -0
- package/dist/cjs/createAuthClient.native.js.map +1 -0
- package/dist/cjs/createAuthClient.test.cjs +88 -0
- package/dist/cjs/createAuthClient.test.native.js +105 -0
- package/dist/cjs/createAuthClient.test.native.js.map +1 -0
- package/dist/cjs/index.cjs +20 -0
- package/dist/cjs/index.native.js +23 -0
- package/dist/cjs/index.native.js.map +1 -0
- package/dist/cjs/server.cjs +106 -0
- package/dist/cjs/server.native.js +228 -0
- package/dist/cjs/server.native.js.map +1 -0
- package/dist/esm/createAuthClient.mjs +226 -0
- package/dist/esm/createAuthClient.mjs.map +1 -0
- package/dist/esm/createAuthClient.native.js +251 -0
- package/dist/esm/createAuthClient.native.js.map +1 -0
- package/dist/esm/createAuthClient.test.mjs +89 -0
- package/dist/esm/createAuthClient.test.mjs.map +1 -0
- package/dist/esm/createAuthClient.test.native.js +103 -0
- package/dist/esm/createAuthClient.test.native.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/index.mjs +2 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/index.native.js +2 -0
- package/dist/esm/index.native.js.map +1 -0
- package/dist/esm/server.mjs +77 -0
- package/dist/esm/server.mjs.map +1 -0
- package/dist/esm/server.native.js +196 -0
- package/dist/esm/server.native.js.map +1 -0
- package/package.json +66 -0
- package/src/createAuthClient.test.ts +86 -0
- package/src/createAuthClient.ts +373 -0
- package/src/index.ts +1 -0
- package/src/server.ts +150 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better-auth helpers for React / React Native applications
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Session persistence in local storage
|
|
6
|
+
* - State management with emitters
|
|
7
|
+
* - Automatic retry on errors
|
|
8
|
+
* - Optional JWT support (for Tauri, React Native)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
createEmitter,
|
|
13
|
+
createStorageValue,
|
|
14
|
+
type Emitter,
|
|
15
|
+
isEqualDeepLite,
|
|
16
|
+
useEmitterValue,
|
|
17
|
+
} from '@o/helpers'
|
|
18
|
+
import { type BetterAuthClientOptions, createAuthClient } from 'better-auth/client'
|
|
19
|
+
|
|
20
|
+
import type { Session, User } from 'better-auth'
|
|
21
|
+
|
|
22
|
+
export interface StorageKeys {
|
|
23
|
+
token: string
|
|
24
|
+
session: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type AuthState<U extends User = User> = {
|
|
28
|
+
state: 'loading' | 'logged-in' | 'logged-out'
|
|
29
|
+
session: Session | null
|
|
30
|
+
user: U | null
|
|
31
|
+
/** JWT token - only populated when useJWT is enabled */
|
|
32
|
+
token: string | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BetterAuthClientProps<
|
|
36
|
+
TUser extends User = User,
|
|
37
|
+
> extends BetterAuthClientOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Callback to transform and type the user object
|
|
40
|
+
* @default (user) => user
|
|
41
|
+
*/
|
|
42
|
+
createUser?: (user: User) => TUser
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Optional callback when authentication state changes
|
|
46
|
+
*/
|
|
47
|
+
onAuthStateChange?: (state: AuthState<TUser>) => void
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Optional callback for handling auth errors
|
|
51
|
+
*/
|
|
52
|
+
onAuthError?: (error: any) => void
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Storage key prefix for local storage
|
|
56
|
+
* @default 'auth'
|
|
57
|
+
*/
|
|
58
|
+
storagePrefix?: string
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Retry delay in milliseconds after auth errors
|
|
62
|
+
* @default 4000
|
|
63
|
+
*/
|
|
64
|
+
retryDelay?: number
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Enable JWT token management for native apps (Tauri, React Native)
|
|
68
|
+
* When false (default), auth uses session cookies forwarded by the server
|
|
69
|
+
* When true, fetches and manages JWT tokens for Authorization header auth
|
|
70
|
+
* @default false
|
|
71
|
+
*/
|
|
72
|
+
useJWT?: boolean
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Cookie names to clear on auth invalidation
|
|
76
|
+
* @default ['better-auth.jwt', 'better-auth.session_token']
|
|
77
|
+
*/
|
|
78
|
+
authCookieNames?: string[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface BetterAuthClientReturn<U extends User = User, TClient = any> {
|
|
82
|
+
clearState: () => void
|
|
83
|
+
authState: ReturnType<typeof createEmitter<AuthState<U>>>
|
|
84
|
+
authClient: TClient
|
|
85
|
+
setAuthClientToken: (props: { token: string; session: string }) => Promise<void>
|
|
86
|
+
clearAuthClientToken: () => void
|
|
87
|
+
clearAllAuth: () => void
|
|
88
|
+
useAuth: () => AuthState<U>
|
|
89
|
+
getAuth: () => AuthState<U> & { loggedIn: boolean }
|
|
90
|
+
getValidToken: () => Promise<string | undefined>
|
|
91
|
+
updateAuthClient: (session: string) => void
|
|
92
|
+
authClientVersion: Emitter<number>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type InferUser<T> = T extends { createUser?: (user: User) => infer R }
|
|
96
|
+
? R extends User
|
|
97
|
+
? R
|
|
98
|
+
: User
|
|
99
|
+
: User
|
|
100
|
+
|
|
101
|
+
export function createBetterAuthClient<const Opts extends BetterAuthClientProps<any>>(
|
|
102
|
+
options: Opts
|
|
103
|
+
): BetterAuthClientReturn<InferUser<Opts>, ReturnType<typeof createAuthClient<Opts>>> {
|
|
104
|
+
type TUser = InferUser<Opts>
|
|
105
|
+
const {
|
|
106
|
+
onAuthStateChange,
|
|
107
|
+
onAuthError,
|
|
108
|
+
createUser,
|
|
109
|
+
storagePrefix = 'auth',
|
|
110
|
+
retryDelay = 4000,
|
|
111
|
+
useJWT = false,
|
|
112
|
+
authCookieNames = ['better-auth.jwt', 'better-auth.session_token'],
|
|
113
|
+
...authClientOptions
|
|
114
|
+
} = options
|
|
115
|
+
|
|
116
|
+
const empty: AuthState<TUser> = {
|
|
117
|
+
state: 'logged-out',
|
|
118
|
+
session: null,
|
|
119
|
+
user: null,
|
|
120
|
+
token: null,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const keysStorage = createStorageValue<StorageKeys>(`${storagePrefix}-keys`)
|
|
124
|
+
const stateStorage = createStorageValue<AuthState<TUser>>(`${storagePrefix}-state`)
|
|
125
|
+
|
|
126
|
+
const createAuthClientWithSession = (session: string) => {
|
|
127
|
+
return createAuthClient({
|
|
128
|
+
...authClientOptions,
|
|
129
|
+
fetchOptions: {
|
|
130
|
+
credentials: 'include',
|
|
131
|
+
headers: session ? { Authorization: `Bearer ${session}` } : undefined,
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let authClient = (() => {
|
|
137
|
+
const existingSession = keysStorage.get()?.session
|
|
138
|
+
return existingSession
|
|
139
|
+
? createAuthClientWithSession(existingSession)
|
|
140
|
+
: createAuthClient({
|
|
141
|
+
...authClientOptions,
|
|
142
|
+
fetchOptions: { credentials: 'include' },
|
|
143
|
+
} as Opts)
|
|
144
|
+
})()
|
|
145
|
+
|
|
146
|
+
const authState = createEmitter<AuthState<TUser>>(
|
|
147
|
+
'authState',
|
|
148
|
+
stateStorage.get() || empty,
|
|
149
|
+
{ comparator: isEqualDeepLite }
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
const authClientVersion = createEmitter<number>('authClientVersion', 0)
|
|
153
|
+
|
|
154
|
+
const setState = (update: Partial<AuthState<TUser>>) => {
|
|
155
|
+
const current = authState.value!
|
|
156
|
+
const next = { ...current, ...update }
|
|
157
|
+
stateStorage.set(next)
|
|
158
|
+
authState.emit(next)
|
|
159
|
+
|
|
160
|
+
// update storage keys
|
|
161
|
+
if (next.token && next.session) {
|
|
162
|
+
keysStorage.set({
|
|
163
|
+
token: next.token,
|
|
164
|
+
session: next.session.token,
|
|
165
|
+
})
|
|
166
|
+
} else if (next.session) {
|
|
167
|
+
keysStorage.set({
|
|
168
|
+
token: '',
|
|
169
|
+
session: next.session.token,
|
|
170
|
+
})
|
|
171
|
+
} else {
|
|
172
|
+
keysStorage.set({ token: '', session: '' })
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
onAuthStateChange?.(next)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const setAuthClientToken = async (props: { token: string; session: string }) => {
|
|
179
|
+
keysStorage.set(props)
|
|
180
|
+
updateAuthClient(props.session)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function updateAuthClient(session: string) {
|
|
184
|
+
authClient = createAuthClientWithSession(session)
|
|
185
|
+
authClientVersion.emit(Math.random())
|
|
186
|
+
subscribeToAuthEffect()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let dispose: Function | null = null
|
|
190
|
+
let retryTimer: ReturnType<typeof setTimeout> | null = null
|
|
191
|
+
// consecutive poll failures drive exponential backoff. a fixed retry clock
|
|
192
|
+
// is self-defeating against a rate limiter: one 429 puts the client on a
|
|
193
|
+
// cadence that keeps it over the limit forever (a real device wedged this
|
|
194
|
+
// way — every 4s retry re-tripped the server's window and the session never
|
|
195
|
+
// confirmed). backoff doubles per consecutive failure up to 60s and resets
|
|
196
|
+
// on any successful poll.
|
|
197
|
+
let consecutiveFailures = 0
|
|
198
|
+
|
|
199
|
+
function subscribeToAuthEffect() {
|
|
200
|
+
dispose?.()
|
|
201
|
+
|
|
202
|
+
dispose = authClient.useSession.subscribe(async (props) => {
|
|
203
|
+
const { data: dataGeneric, isPending, error } = props
|
|
204
|
+
|
|
205
|
+
if (error) {
|
|
206
|
+
onAuthError?.(error)
|
|
207
|
+
// if no persisted session, transition to logged-out instead of staying loading
|
|
208
|
+
const hasPersistedSession = !!keysStorage.get()?.session
|
|
209
|
+
if (!hasPersistedSession) {
|
|
210
|
+
setState({ state: 'logged-out', session: null, user: null })
|
|
211
|
+
}
|
|
212
|
+
consecutiveFailures++
|
|
213
|
+
scheduleAuthRetry(Math.min(retryDelay * 2 ** (consecutiveFailures - 1), 60_000))
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
consecutiveFailures = 0
|
|
218
|
+
|
|
219
|
+
const data = dataGeneric as
|
|
220
|
+
| undefined
|
|
221
|
+
| {
|
|
222
|
+
session?: AuthState<TUser>['session']
|
|
223
|
+
user?: AuthState<TUser>['user']
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// if we have a persisted session but server hasn't confirmed yet, stay loading
|
|
227
|
+
const hasPersistedSession = !!keysStorage.get()?.session
|
|
228
|
+
const nextState = isPending
|
|
229
|
+
? 'loading'
|
|
230
|
+
: data?.session
|
|
231
|
+
? 'logged-in'
|
|
232
|
+
: hasPersistedSession && data === undefined
|
|
233
|
+
? 'loading'
|
|
234
|
+
: 'logged-out'
|
|
235
|
+
|
|
236
|
+
// only update session/user when we have definitive data
|
|
237
|
+
const sessionUpdate =
|
|
238
|
+
nextState === 'loading'
|
|
239
|
+
? {}
|
|
240
|
+
: {
|
|
241
|
+
session: data?.session ?? null,
|
|
242
|
+
user: data?.user ? (createUser ? createUser(data.user) : data.user) : null,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// detect new session
|
|
246
|
+
const previousSession = authState.value?.session
|
|
247
|
+
const isNewSession =
|
|
248
|
+
data?.session &&
|
|
249
|
+
(!previousSession ||
|
|
250
|
+
previousSession.id !== data.session.id ||
|
|
251
|
+
previousSession.userId !== data.session.userId)
|
|
252
|
+
|
|
253
|
+
setState({
|
|
254
|
+
state: nextState,
|
|
255
|
+
...sessionUpdate,
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
// fetch JWT token when useJWT is enabled (for native/tauri apps)
|
|
259
|
+
if (useJWT && data?.session && (isNewSession || !authState.value.token)) {
|
|
260
|
+
if (isNewSession && authState.value.token) {
|
|
261
|
+
setState({ token: null })
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
getValidToken().then((token) => {
|
|
265
|
+
if (token) {
|
|
266
|
+
setState({ token })
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function scheduleAuthRetry(delayMs: number) {
|
|
274
|
+
if (retryTimer) clearTimeout(retryTimer)
|
|
275
|
+
retryTimer = setTimeout(() => {
|
|
276
|
+
retryTimer = null
|
|
277
|
+
subscribeToAuthEffect()
|
|
278
|
+
}, delayMs)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function getValidToken(): Promise<string | undefined> {
|
|
282
|
+
const res = await authClient.$fetch('/token')
|
|
283
|
+
if (res.error) {
|
|
284
|
+
console.error(`Error fetching token: ${res.error.statusText}`)
|
|
285
|
+
return undefined
|
|
286
|
+
}
|
|
287
|
+
return (res.data as any)?.token as string | undefined
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const clearAuthClientToken = () => {
|
|
291
|
+
keysStorage.remove()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function clearAuthCookies() {
|
|
295
|
+
if (typeof document === 'undefined') return
|
|
296
|
+
|
|
297
|
+
for (const cookieName of authCookieNames) {
|
|
298
|
+
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`
|
|
299
|
+
const domain = window.location.hostname
|
|
300
|
+
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${domain}`
|
|
301
|
+
if (domain.startsWith('.')) {
|
|
302
|
+
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${domain.slice(1)}`
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function clearAllAuth() {
|
|
308
|
+
clearAuthCookies()
|
|
309
|
+
clearState()
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const getAuth = () => {
|
|
313
|
+
const state = authState?.value || empty
|
|
314
|
+
return { ...state, loggedIn: !!state.session }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const useAuth = () => {
|
|
318
|
+
return useEmitterValue(authState) || empty
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function clearState() {
|
|
322
|
+
keysStorage.remove()
|
|
323
|
+
stateStorage.remove()
|
|
324
|
+
setState(empty)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
subscribeToAuthEffect()
|
|
328
|
+
|
|
329
|
+
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
330
|
+
const cleanup = () => {
|
|
331
|
+
dispose?.()
|
|
332
|
+
if (retryTimer) clearTimeout(retryTimer)
|
|
333
|
+
}
|
|
334
|
+
window.addEventListener('beforeunload', cleanup)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const proxiedAuthClient = new Proxy(authClient, {
|
|
338
|
+
get(_target, key) {
|
|
339
|
+
if (key === 'signOut') {
|
|
340
|
+
return async () => {
|
|
341
|
+
// an explicit empty body makes better-fetch send JSON instead of a
|
|
342
|
+
// bodyless POST, which Better Auth rejects with 415.
|
|
343
|
+
const result = await authClient.$fetch('/sign-out', {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
body: {},
|
|
346
|
+
})
|
|
347
|
+
if (result.error) throw result.error
|
|
348
|
+
|
|
349
|
+
clearState()
|
|
350
|
+
if (typeof window !== 'undefined') {
|
|
351
|
+
window.location?.reload?.()
|
|
352
|
+
}
|
|
353
|
+
return result
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return Reflect.get(authClient, key)
|
|
357
|
+
},
|
|
358
|
+
}) as ReturnType<typeof createAuthClient<Opts>>
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
authClientVersion,
|
|
362
|
+
clearState,
|
|
363
|
+
authState,
|
|
364
|
+
authClient: proxiedAuthClient,
|
|
365
|
+
setAuthClientToken,
|
|
366
|
+
clearAuthClientToken,
|
|
367
|
+
clearAllAuth,
|
|
368
|
+
useAuth,
|
|
369
|
+
getAuth,
|
|
370
|
+
getValidToken,
|
|
371
|
+
updateAuthClient,
|
|
372
|
+
}
|
|
373
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './createAuthClient'
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side auth utilities for better-auth
|
|
3
|
+
* - Session validation via cookies (web)
|
|
4
|
+
* - JWT validation via JWKS (native apps)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from 'jose'
|
|
8
|
+
|
|
9
|
+
export interface ValidateTokenOptions {
|
|
10
|
+
/** base URL for the auth server (e.g., https://myapp.com) */
|
|
11
|
+
baseUrl?: string
|
|
12
|
+
/** optional issuer override for CI/test environments */
|
|
13
|
+
forceIssuer?: string
|
|
14
|
+
/** JWKS endpoint path, defaults to /api/auth/jwks */
|
|
15
|
+
jwksPath?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AuthData {
|
|
19
|
+
id: string
|
|
20
|
+
email?: string
|
|
21
|
+
role: 'admin' | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class NotAuthenticatedError extends Error {}
|
|
25
|
+
export class InvalidTokenError extends Error {}
|
|
26
|
+
|
|
27
|
+
export type AuthServer = {
|
|
28
|
+
api: {
|
|
29
|
+
getSession: (opts: { headers: Headers }) =>
|
|
30
|
+
| Promise<{
|
|
31
|
+
user: { id: string; email?: string | null; role?: string | null }
|
|
32
|
+
} | null>
|
|
33
|
+
| Promise<any>
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get auth data from request - tries session first, then a JWKS-verified JWT.
|
|
39
|
+
* Session: web cookies (forwarded by zero) and native bearer tokens resolved by
|
|
40
|
+
* the better-auth bearer() plugin — both handled by getSession.
|
|
41
|
+
* JWT: only for servers using the jwt() plugin instead of bearer().
|
|
42
|
+
*/
|
|
43
|
+
export async function getAuthDataFromRequest(
|
|
44
|
+
authServer: AuthServer,
|
|
45
|
+
req: Request,
|
|
46
|
+
tokenOptions?: ValidateTokenOptions
|
|
47
|
+
): Promise<AuthData | null> {
|
|
48
|
+
const authHeader = req.headers.get('authorization')
|
|
49
|
+
|
|
50
|
+
// try session-based auth first, passing the request headers THROUGH UNTOUCHED:
|
|
51
|
+
// - web sends the forwarded session cookie.
|
|
52
|
+
// - native (React Native / Tauri) sends `Authorization: Bearer <session-token>`,
|
|
53
|
+
// which the better-auth bearer() plugin verifies and rewrites into the session
|
|
54
|
+
// cookie internally (with the correct cookie name).
|
|
55
|
+
// do NOT rewrite the Cookie header from the bearer value: the bearer token is
|
|
56
|
+
// not a `name=value` cookie, so it never matched a real session cookie, and
|
|
57
|
+
// overwriting an existing forwarded cookie with it silently dropped authenticated
|
|
58
|
+
// web requests (cookie + bearer both present) to an `anon` identity.
|
|
59
|
+
try {
|
|
60
|
+
const session = await authServer.api.getSession({ headers: req.headers })
|
|
61
|
+
if (session?.user) {
|
|
62
|
+
return {
|
|
63
|
+
id: session.user.id,
|
|
64
|
+
email: session.user.email || undefined,
|
|
65
|
+
role: session.user.role === 'admin' ? 'admin' : undefined,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.warn(`Error validating session`, err)
|
|
70
|
+
// session auth failed, try JWT
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// fall back to JWKS-verified JWT for servers using the jwt() plugin (no
|
|
74
|
+
// bearer() plugin to resolve the token above).
|
|
75
|
+
const jwtToken = authHeader?.replace('Bearer ', '')
|
|
76
|
+
|
|
77
|
+
if (jwtToken) {
|
|
78
|
+
try {
|
|
79
|
+
const payload = await validateToken(jwtToken, tokenOptions)
|
|
80
|
+
const userId = (payload as any)?.id || payload?.sub
|
|
81
|
+
if (userId) {
|
|
82
|
+
return {
|
|
83
|
+
id: userId as string,
|
|
84
|
+
email: (payload as any).email as string | undefined,
|
|
85
|
+
role: (payload as any).role === 'admin' ? 'admin' : undefined,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (!(err instanceof InvalidTokenError)) {
|
|
90
|
+
throw err
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// jwt validation for native apps
|
|
99
|
+
|
|
100
|
+
export async function validateToken(
|
|
101
|
+
token: string,
|
|
102
|
+
options?: ValidateTokenOptions
|
|
103
|
+
): Promise<JWTPayload> {
|
|
104
|
+
const {
|
|
105
|
+
baseUrl = process.env.ONE_SERVER_URL,
|
|
106
|
+
forceIssuer = process.env.FORCE_ISSUER || '',
|
|
107
|
+
jwksPath = '/api/auth/jwks',
|
|
108
|
+
} = options || {}
|
|
109
|
+
|
|
110
|
+
if (!baseUrl) {
|
|
111
|
+
throw new Error(`No baseURL!`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const normalizedBaseUrl = removeTrailingSlash(baseUrl)
|
|
115
|
+
const url = `${forceIssuer || normalizedBaseUrl}${jwksPath}`
|
|
116
|
+
|
|
117
|
+
// create fresh JWKS fetcher each time to avoid stale key cache issues
|
|
118
|
+
const JWKS = createRemoteJWKSet(new URL(url))
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const verifyOptions = forceIssuer
|
|
122
|
+
? {}
|
|
123
|
+
: {
|
|
124
|
+
issuer: normalizedBaseUrl,
|
|
125
|
+
audience: normalizedBaseUrl,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { payload } = await jwtVerify(token, JWKS, verifyOptions)
|
|
129
|
+
|
|
130
|
+
return payload
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw new InvalidTokenError(`${error}`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function isValidJWT(
|
|
137
|
+
token: string,
|
|
138
|
+
options: ValidateTokenOptions
|
|
139
|
+
): Promise<boolean> {
|
|
140
|
+
try {
|
|
141
|
+
await validateToken(token, options)
|
|
142
|
+
return true
|
|
143
|
+
} catch {
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function removeTrailingSlash(str: string) {
|
|
149
|
+
return str.replace(/\/$/, '')
|
|
150
|
+
}
|