auth-vir 5.0.0 → 5.0.2
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/package.json +8 -8
- package/src/auth-client/backend-auth.client.ts +8 -0
- package/src/auth.ts +9 -1
- package/dist/auth-client/backend-auth.client.d.ts +0 -263
- package/dist/auth-client/backend-auth.client.js +0 -391
- package/dist/auth-client/frontend-auth.client.d.ts +0 -113
- package/dist/auth-client/frontend-auth.client.js +0 -131
- package/dist/auth-client/is-session-refresh-ready.d.ts +0 -23
- package/dist/auth-client/is-session-refresh-ready.js +0 -21
- package/dist/auth.d.ts +0 -74
- package/dist/auth.js +0 -128
- package/dist/cookie.d.ts +0 -111
- package/dist/cookie.js +0 -137
- package/dist/csrf-token.d.ts +0 -33
- package/dist/csrf-token.js +0 -42
- package/dist/generated/browser.d.ts +0 -9
- package/dist/generated/browser.js +0 -17
- package/dist/generated/client.d.ts +0 -26
- package/dist/generated/client.js +0 -32
- package/dist/generated/commonInputTypes.d.ts +0 -122
- package/dist/generated/commonInputTypes.js +0 -1
- package/dist/generated/enums.d.ts +0 -1
- package/dist/generated/enums.js +0 -10
- package/dist/generated/internal/class.d.ts +0 -126
- package/dist/generated/internal/class.js +0 -85
- package/dist/generated/internal/prismaNamespace.d.ts +0 -545
- package/dist/generated/internal/prismaNamespace.js +0 -102
- package/dist/generated/internal/prismaNamespaceBrowser.d.ts +0 -75
- package/dist/generated/internal/prismaNamespaceBrowser.js +0 -70
- package/dist/generated/models/User.d.ts +0 -980
- package/dist/generated/models/User.js +0 -1
- package/dist/generated/models.d.ts +0 -2
- package/dist/generated/models.js +0 -1
- package/dist/generated/shapes.gen.d.ts +0 -8
- package/dist/generated/shapes.gen.js +0 -11
- package/dist/hash.d.ts +0 -42
- package/dist/hash.js +0 -52
- package/dist/headers.d.ts +0 -19
- package/dist/headers.js +0 -32
- package/dist/index.d.ts +0 -11
- package/dist/index.js +0 -11
- package/dist/jwt/jwt-keys.d.ts +0 -44
- package/dist/jwt/jwt-keys.js +0 -57
- package/dist/jwt/jwt-keys.script.d.ts +0 -1
- package/dist/jwt/jwt-keys.script.js +0 -3
- package/dist/jwt/jwt.d.ts +0 -126
- package/dist/jwt/jwt.js +0 -109
- package/dist/jwt/user-jwt.d.ts +0 -44
- package/dist/jwt/user-jwt.js +0 -53
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
import { ensureArray, } from '@augment-vir/common';
|
|
2
|
-
import { calculateRelativeDate, createUtcFullDate, getNowInUtcTimezone, isDateAfter, } from 'date-vir';
|
|
3
|
-
import { extractUserIdFromRequestHeaders, generateLogoutHeaders, generateSuccessfulLoginHeaders, insecureExtractUserIdFromCookieAlone, } from '../auth.js';
|
|
4
|
-
import { AuthCookie, generateAuthCookie, generateCsrfCookie } from '../cookie.js';
|
|
5
|
-
import { AuthHeaderName, mergeHeaderValues } from '../headers.js';
|
|
6
|
-
import { parseJwtKeys } from '../jwt/jwt-keys.js';
|
|
7
|
-
import { defaultAllowedClockSkew } from '../jwt/jwt.js';
|
|
8
|
-
import { isSessionRefreshReady } from './is-session-refresh-ready.js';
|
|
9
|
-
const defaultSessionIdleTimeout = {
|
|
10
|
-
minutes: 20,
|
|
11
|
-
};
|
|
12
|
-
const defaultSessionRefreshStartTime = {
|
|
13
|
-
minutes: 2,
|
|
14
|
-
};
|
|
15
|
-
const defaultMaxSessionDuration = {
|
|
16
|
-
days: 1.5,
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* An auth client for creating and validating JWTs embedded in cookies. This should only be used in
|
|
20
|
-
* a backend environment as it accesses native Node packages.
|
|
21
|
-
*
|
|
22
|
-
* @category Auth : Host
|
|
23
|
-
* @category Clients
|
|
24
|
-
*/
|
|
25
|
-
export class BackendAuthClient {
|
|
26
|
-
config;
|
|
27
|
-
cachedParsedJwtKeys = {};
|
|
28
|
-
constructor(config) {
|
|
29
|
-
this.config = config;
|
|
30
|
-
}
|
|
31
|
-
/** Conditionally logs a message if logging is enabled for the given user context. */
|
|
32
|
-
logForUser(params, message, extra) {
|
|
33
|
-
if (this.config.enableLogging?.(params)) {
|
|
34
|
-
const extraData = {
|
|
35
|
-
userId: params.userId,
|
|
36
|
-
...extra,
|
|
37
|
-
};
|
|
38
|
-
if (this.config.log) {
|
|
39
|
-
this.config.log(message, extraData);
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
console.info(`[auth-vir] ${message}`, extraData);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/** Get all the parameters used for cookie generation. */
|
|
47
|
-
async getCookieParams({ isSignUpCookie, requestHeaders, }) {
|
|
48
|
-
const serviceOrigin = requestHeaders
|
|
49
|
-
? await this.config.generateServiceOrigin?.({
|
|
50
|
-
requestHeaders,
|
|
51
|
-
})
|
|
52
|
-
: undefined;
|
|
53
|
-
return {
|
|
54
|
-
cookieDuration: this.config.userSessionIdleTimeout || defaultSessionIdleTimeout,
|
|
55
|
-
hostOrigin: serviceOrigin || this.config.serviceOrigin,
|
|
56
|
-
jwtParams: await this.getJwtParams(),
|
|
57
|
-
isDev: this.config.isDev,
|
|
58
|
-
authCookie: isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
/** Calls the provided `getUserFromDatabase` config. */
|
|
62
|
-
async getDatabaseUser({ isSignUpCookie, userId, assumingUser, requestHeaders, }) {
|
|
63
|
-
if (!userId) {
|
|
64
|
-
return undefined;
|
|
65
|
-
}
|
|
66
|
-
const authenticatedUser = await this.config.getUserFromDatabase({
|
|
67
|
-
assumingUser,
|
|
68
|
-
userId,
|
|
69
|
-
isSignUpCookie,
|
|
70
|
-
requestHeaders,
|
|
71
|
-
});
|
|
72
|
-
if (!authenticatedUser) {
|
|
73
|
-
this.logForUser({
|
|
74
|
-
user: undefined,
|
|
75
|
-
userId,
|
|
76
|
-
assumedUserParams: assumingUser,
|
|
77
|
-
}, 'getUserFromDatabase returned no user', {
|
|
78
|
-
isSignUpCookie,
|
|
79
|
-
});
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
return authenticatedUser;
|
|
83
|
-
}
|
|
84
|
-
/** Creates a `'cookie-set'` header to refresh the user's session cookie. */
|
|
85
|
-
async createCookieRefreshHeaders({ userIdResult, requestHeaders, }) {
|
|
86
|
-
const now = getNowInUtcTimezone();
|
|
87
|
-
const clockSkew = this.config.allowedClockSkew || defaultAllowedClockSkew;
|
|
88
|
-
/** Double check that the JWT hasn't already expired (with clock skew tolerance). */
|
|
89
|
-
const isExpiredAlready = isDateAfter({
|
|
90
|
-
fullDate: now,
|
|
91
|
-
relativeTo: calculateRelativeDate(userIdResult.jwtExpiration, clockSkew),
|
|
92
|
-
});
|
|
93
|
-
if (isExpiredAlready) {
|
|
94
|
-
this.logForUser({
|
|
95
|
-
user: undefined,
|
|
96
|
-
userId: userIdResult.userId,
|
|
97
|
-
assumedUserParams: undefined,
|
|
98
|
-
}, 'Session refresh denied: JWT already expired (even with clock skew tolerance)', {
|
|
99
|
-
jwtExpiration: userIdResult.jwtExpiration,
|
|
100
|
-
now: JSON.stringify(now),
|
|
101
|
-
});
|
|
102
|
-
return undefined;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Check if the session has exceeded the max session duration. If so, don't refresh the
|
|
106
|
-
* session and let it expire naturally.
|
|
107
|
-
*/
|
|
108
|
-
const maxSessionDuration = this.config.maxSessionDuration || defaultMaxSessionDuration;
|
|
109
|
-
if (userIdResult.sessionStartedAt) {
|
|
110
|
-
const sessionStartDate = createUtcFullDate(userIdResult.sessionStartedAt);
|
|
111
|
-
const maxSessionEndDate = calculateRelativeDate(sessionStartDate, maxSessionDuration);
|
|
112
|
-
const isSessionExpired = isDateAfter({
|
|
113
|
-
fullDate: now,
|
|
114
|
-
relativeTo: maxSessionEndDate,
|
|
115
|
-
});
|
|
116
|
-
if (isSessionExpired) {
|
|
117
|
-
this.logForUser({
|
|
118
|
-
user: undefined,
|
|
119
|
-
userId: userIdResult.userId,
|
|
120
|
-
assumedUserParams: undefined,
|
|
121
|
-
}, 'Session refresh denied: max session duration exceeded', {
|
|
122
|
-
sessionStartedAt: userIdResult.sessionStartedAt,
|
|
123
|
-
maxSessionEndDate: JSON.stringify(maxSessionEndDate),
|
|
124
|
-
now: JSON.stringify(now),
|
|
125
|
-
});
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
const sessionRefreshStartTime = this.config.sessionRefreshStartTime || defaultSessionRefreshStartTime;
|
|
130
|
-
const isRefreshReady = isSessionRefreshReady({
|
|
131
|
-
now,
|
|
132
|
-
jwtIssuedAt: userIdResult.jwtIssuedAt,
|
|
133
|
-
sessionRefreshStartTime,
|
|
134
|
-
});
|
|
135
|
-
if (isRefreshReady) {
|
|
136
|
-
const isSignUpCookie = userIdResult.cookieName === AuthCookie.SignUp;
|
|
137
|
-
const cookieParams = await this.getCookieParams({
|
|
138
|
-
isSignUpCookie,
|
|
139
|
-
requestHeaders,
|
|
140
|
-
});
|
|
141
|
-
const authCookie = await generateAuthCookie({
|
|
142
|
-
csrfToken: userIdResult.csrfToken,
|
|
143
|
-
userId: userIdResult.userId,
|
|
144
|
-
sessionStartedAt: userIdResult.sessionStartedAt || Date.now(),
|
|
145
|
-
}, cookieParams);
|
|
146
|
-
const csrfCookie = generateCsrfCookie(userIdResult.csrfToken, cookieParams);
|
|
147
|
-
return {
|
|
148
|
-
'set-cookie': [
|
|
149
|
-
authCookie,
|
|
150
|
-
csrfCookie,
|
|
151
|
-
],
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
this.logForUser({
|
|
156
|
-
user: undefined,
|
|
157
|
-
userId: userIdResult.userId,
|
|
158
|
-
assumedUserParams: undefined,
|
|
159
|
-
}, 'Session refresh skipped: not yet ready for refresh', {
|
|
160
|
-
jwtIssuedAt: userIdResult.jwtIssuedAt,
|
|
161
|
-
sessionRefreshStartTime,
|
|
162
|
-
});
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
/** Reads the user's assumed user headers and, if configured, gets the assumed user. */
|
|
167
|
-
async getAssumedUser({ requestHeaders, user, }) {
|
|
168
|
-
if (!this.config.assumeUser || !(await this.config.assumeUser.canAssumeUser(user))) {
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
const assumedUserHeader = ensureArray(requestHeaders[this.config.assumedUserHeaderName || AuthHeaderName.AssumedUser])[0];
|
|
172
|
-
if (!assumedUserHeader) {
|
|
173
|
-
return undefined;
|
|
174
|
-
}
|
|
175
|
-
const parsedAssumedUserData = await this.config.assumeUser.parseAssumedUserHeaderValue(assumedUserHeader);
|
|
176
|
-
if (!parsedAssumedUserData || !parsedAssumedUserData.userId) {
|
|
177
|
-
return undefined;
|
|
178
|
-
}
|
|
179
|
-
const assumedUser = await this.getDatabaseUser({
|
|
180
|
-
isSignUpCookie: false,
|
|
181
|
-
userId: parsedAssumedUserData.userId,
|
|
182
|
-
assumingUser: parsedAssumedUserData.assumedUserParams,
|
|
183
|
-
requestHeaders,
|
|
184
|
-
});
|
|
185
|
-
return assumedUser;
|
|
186
|
-
}
|
|
187
|
-
/** Securely extract a user from their request headers. */
|
|
188
|
-
async getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }) {
|
|
189
|
-
const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth);
|
|
190
|
-
if (!userIdResult) {
|
|
191
|
-
this.logForUser({
|
|
192
|
-
user: undefined,
|
|
193
|
-
userId: undefined,
|
|
194
|
-
assumedUserParams: undefined,
|
|
195
|
-
}, 'getSecureUser: failed to extract user ID from request headers (invalid JWT, missing cookie, or CSRF mismatch)', {
|
|
196
|
-
isSignUpCookie,
|
|
197
|
-
});
|
|
198
|
-
return undefined;
|
|
199
|
-
}
|
|
200
|
-
const user = await this.getDatabaseUser({
|
|
201
|
-
userId: userIdResult.userId,
|
|
202
|
-
assumingUser: undefined,
|
|
203
|
-
isSignUpCookie,
|
|
204
|
-
requestHeaders,
|
|
205
|
-
});
|
|
206
|
-
if (!user) {
|
|
207
|
-
this.logForUser({
|
|
208
|
-
user: undefined,
|
|
209
|
-
userId: userIdResult.userId,
|
|
210
|
-
assumedUserParams: undefined,
|
|
211
|
-
}, 'getSecureUser: user not found in database', {
|
|
212
|
-
isSignUpCookie,
|
|
213
|
-
});
|
|
214
|
-
return undefined;
|
|
215
|
-
}
|
|
216
|
-
const assumedUser = await this.getAssumedUser({
|
|
217
|
-
requestHeaders,
|
|
218
|
-
user,
|
|
219
|
-
});
|
|
220
|
-
const cookieRefreshHeaders = allowUserAuthRefresh
|
|
221
|
-
? await this.createCookieRefreshHeaders({
|
|
222
|
-
userIdResult,
|
|
223
|
-
requestHeaders,
|
|
224
|
-
})
|
|
225
|
-
: undefined;
|
|
226
|
-
/**
|
|
227
|
-
* Always include the CSRF cookie so it gets re-established if the browser clears it. When
|
|
228
|
-
* session refresh fires, its headers already include a CSRF cookie.
|
|
229
|
-
*/
|
|
230
|
-
const csrfCookie = generateCsrfCookie(userIdResult.csrfToken, {
|
|
231
|
-
hostOrigin: (await this.config.generateServiceOrigin?.({
|
|
232
|
-
requestHeaders,
|
|
233
|
-
})) || this.config.serviceOrigin,
|
|
234
|
-
isDev: this.config.isDev,
|
|
235
|
-
});
|
|
236
|
-
return {
|
|
237
|
-
user: assumedUser || user,
|
|
238
|
-
isAssumed: !!assumedUser,
|
|
239
|
-
responseHeaders: {
|
|
240
|
-
'set-cookie': mergeHeaderValues(cookieRefreshHeaders?.['set-cookie'], csrfCookie),
|
|
241
|
-
},
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Get all the JWT params used when creating the auth cookie, in case you need them for
|
|
246
|
-
* something else too.
|
|
247
|
-
*/
|
|
248
|
-
async getJwtParams() {
|
|
249
|
-
const rawJwtKeys = await this.config.getJwtKeys();
|
|
250
|
-
const cacheKey = JSON.stringify(rawJwtKeys);
|
|
251
|
-
const cachedParsedKeys = this.cachedParsedJwtKeys[cacheKey];
|
|
252
|
-
const parsedKeys = cachedParsedKeys || (await parseJwtKeys(rawJwtKeys));
|
|
253
|
-
if (!cachedParsedKeys) {
|
|
254
|
-
this.cachedParsedJwtKeys = {
|
|
255
|
-
[cacheKey]: parsedKeys,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
return {
|
|
259
|
-
jwtKeys: parsedKeys,
|
|
260
|
-
audience: 'server-context',
|
|
261
|
-
issuer: 'server-auth',
|
|
262
|
-
jwtDuration: this.config.userSessionIdleTimeout || defaultSessionIdleTimeout,
|
|
263
|
-
allowedClockSkew: this.config.allowedClockSkew || defaultAllowedClockSkew,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
/** Use these headers to log out the user. */
|
|
267
|
-
async createLogoutHeaders(params) {
|
|
268
|
-
const signUpCookieHeaders = params.allCookies || params.isSignUpCookie
|
|
269
|
-
? generateLogoutHeaders(await this.getCookieParams({
|
|
270
|
-
isSignUpCookie: true,
|
|
271
|
-
requestHeaders: undefined,
|
|
272
|
-
}))
|
|
273
|
-
: undefined;
|
|
274
|
-
const authCookieHeaders = params.allCookies || !params.isSignUpCookie
|
|
275
|
-
? generateLogoutHeaders(await this.getCookieParams({
|
|
276
|
-
isSignUpCookie: false,
|
|
277
|
-
requestHeaders: undefined,
|
|
278
|
-
}))
|
|
279
|
-
: undefined;
|
|
280
|
-
return {
|
|
281
|
-
'set-cookie': mergeHeaderValues(signUpCookieHeaders?.['set-cookie'], authCookieHeaders?.['set-cookie']),
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Refreshes a login session by reissuing the auth cookie with the same CSRF token instead of
|
|
286
|
-
* generating a new one.
|
|
287
|
-
*/
|
|
288
|
-
async refreshLoginHeaders({ userId, cookieParams, existingUserIdResult, }) {
|
|
289
|
-
const authCookie = await generateAuthCookie({
|
|
290
|
-
csrfToken: existingUserIdResult.csrfToken,
|
|
291
|
-
userId,
|
|
292
|
-
sessionStartedAt: existingUserIdResult.sessionStartedAt,
|
|
293
|
-
}, cookieParams);
|
|
294
|
-
const csrfCookie = generateCsrfCookie(existingUserIdResult.csrfToken, cookieParams);
|
|
295
|
-
return {
|
|
296
|
-
'set-cookie': [
|
|
297
|
-
authCookie,
|
|
298
|
-
csrfCookie,
|
|
299
|
-
],
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
/** Use these headers to log a user in. */
|
|
303
|
-
async createLoginHeaders({ userId, requestHeaders, isSignUpCookie, }) {
|
|
304
|
-
const oppositeCookieName = isSignUpCookie ? AuthCookie.Auth : AuthCookie.SignUp;
|
|
305
|
-
const hasExistingOppositeCookie = requestHeaders.cookie?.includes(`${oppositeCookieName}=`);
|
|
306
|
-
const discardOppositeCookieHeaders = hasExistingOppositeCookie
|
|
307
|
-
? generateLogoutHeaders(await this.getCookieParams({
|
|
308
|
-
isSignUpCookie: !isSignUpCookie,
|
|
309
|
-
requestHeaders,
|
|
310
|
-
}))
|
|
311
|
-
: undefined;
|
|
312
|
-
const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth);
|
|
313
|
-
const cookieParams = await this.getCookieParams({
|
|
314
|
-
isSignUpCookie,
|
|
315
|
-
requestHeaders,
|
|
316
|
-
});
|
|
317
|
-
const newCookieHeaders = existingUserIdResult
|
|
318
|
-
? await this.refreshLoginHeaders({
|
|
319
|
-
userId,
|
|
320
|
-
cookieParams,
|
|
321
|
-
existingUserIdResult,
|
|
322
|
-
})
|
|
323
|
-
: await generateSuccessfulLoginHeaders(userId, cookieParams);
|
|
324
|
-
return {
|
|
325
|
-
...newCookieHeaders,
|
|
326
|
-
'set-cookie': mergeHeaderValues(newCookieHeaders['set-cookie'], discardOppositeCookieHeaders?.['set-cookie']),
|
|
327
|
-
...(isSignUpCookie
|
|
328
|
-
? {
|
|
329
|
-
[AuthHeaderName.IsSignUpAuth]: 'true',
|
|
330
|
-
}
|
|
331
|
-
: {}),
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
/** Combines `.getInsecureUser()` and `.getSecureUser()` into one method. */
|
|
335
|
-
async getInsecureOrSecureUser(params) {
|
|
336
|
-
const secureUser = await this.getSecureUser(params);
|
|
337
|
-
if (secureUser) {
|
|
338
|
-
return {
|
|
339
|
-
secureUser,
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
343
|
-
const insecureUser = await this.getInsecureUser(params);
|
|
344
|
-
return insecureUser
|
|
345
|
-
? {
|
|
346
|
-
insecureUser,
|
|
347
|
-
}
|
|
348
|
-
: {};
|
|
349
|
-
}
|
|
350
|
-
/**
|
|
351
|
-
* @deprecated This only half authenticates the user. It should only be used in circumstances
|
|
352
|
-
* where JavaScript cannot be used to attach the CSRF token header to the request (like when
|
|
353
|
-
* opening a PDF file). Use `.getSecureUser()` instead, whenever possible.
|
|
354
|
-
*/
|
|
355
|
-
async getInsecureUser({ requestHeaders, allowUserAuthRefresh, }) {
|
|
356
|
-
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
357
|
-
const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookie.Auth);
|
|
358
|
-
if (!userIdResult) {
|
|
359
|
-
this.logForUser({
|
|
360
|
-
user: undefined,
|
|
361
|
-
userId: undefined,
|
|
362
|
-
assumedUserParams: undefined,
|
|
363
|
-
}, 'getInsecureUser: failed to extract user ID from cookie (invalid JWT or missing cookie)');
|
|
364
|
-
return undefined;
|
|
365
|
-
}
|
|
366
|
-
const user = await this.getDatabaseUser({
|
|
367
|
-
isSignUpCookie: false,
|
|
368
|
-
userId: userIdResult.userId,
|
|
369
|
-
assumingUser: undefined,
|
|
370
|
-
requestHeaders,
|
|
371
|
-
});
|
|
372
|
-
if (!user) {
|
|
373
|
-
this.logForUser({
|
|
374
|
-
user: undefined,
|
|
375
|
-
userId: userIdResult.userId,
|
|
376
|
-
assumedUserParams: undefined,
|
|
377
|
-
}, 'getInsecureUser: user not found in database');
|
|
378
|
-
return undefined;
|
|
379
|
-
}
|
|
380
|
-
const refreshHeaders = allowUserAuthRefresh &&
|
|
381
|
-
(await this.createCookieRefreshHeaders({
|
|
382
|
-
userIdResult,
|
|
383
|
-
requestHeaders,
|
|
384
|
-
}));
|
|
385
|
-
return {
|
|
386
|
-
user,
|
|
387
|
-
isAssumed: false,
|
|
388
|
-
responseHeaders: refreshHeaders || {},
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { type createBlockingInterval, type JsonCompatibleObject, type MaybePromise, type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
|
|
2
|
-
import { type AnyDuration } from 'date-vir';
|
|
3
|
-
import { type EmptyObject } from 'type-fest';
|
|
4
|
-
import { type CsrfHeaderNameOption } from '../csrf-token.js';
|
|
5
|
-
/**
|
|
6
|
-
* Config for {@link FrontendAuthClient}.
|
|
7
|
-
*
|
|
8
|
-
* @category Internal
|
|
9
|
-
*/
|
|
10
|
-
export type FrontendAuthClientConfig = Readonly<{
|
|
11
|
-
csrf: Readonly<CsrfHeaderNameOption>;
|
|
12
|
-
}> & PartialWithUndefined<{
|
|
13
|
-
/**
|
|
14
|
-
* Determine if the current user can assume the identity of another user. If this is not
|
|
15
|
-
* defined, all users will be blocked from assuming other user identities.
|
|
16
|
-
*/
|
|
17
|
-
canAssumeUser: () => MaybePromise<boolean>;
|
|
18
|
-
/** Called whenever the current user becomes unauthorized and their CSRF token is wiped. */
|
|
19
|
-
authClearedCallback: () => MaybePromise<void>;
|
|
20
|
-
/**
|
|
21
|
-
* Performs automatic checks on an interval to see if the user is still authenticated. Omit
|
|
22
|
-
* this to turn off automatic checks.
|
|
23
|
-
*/
|
|
24
|
-
checkUser: {
|
|
25
|
-
/**
|
|
26
|
-
* Get a response from the backend to see if the user is still authenticated. If the
|
|
27
|
-
* response returns a non-authorized status, the user is wiped. Any other status is
|
|
28
|
-
* ignored.
|
|
29
|
-
*
|
|
30
|
-
* If the user is not currently authorized, this should return `undefined` to prevent
|
|
31
|
-
* unnecessary network traffic.
|
|
32
|
-
*
|
|
33
|
-
* This will be called any time the user interacts with the page, debounced by the
|
|
34
|
-
* adjacent `debounce` property.
|
|
35
|
-
*/
|
|
36
|
-
performCheck: () => MaybePromise<SelectFrom<Response, {
|
|
37
|
-
status: true;
|
|
38
|
-
}> | undefined>;
|
|
39
|
-
/**
|
|
40
|
-
* Debounce for firing `performCheck`.
|
|
41
|
-
*
|
|
42
|
-
* @default {minutes: 1}
|
|
43
|
-
*/
|
|
44
|
-
debounce?: AnyDuration | undefined;
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* Overwrite the header name used for tracking is an admin is assuming the identity of
|
|
48
|
-
* another user.
|
|
49
|
-
*/
|
|
50
|
-
assumedUserHeaderName: string;
|
|
51
|
-
overrides: PartialWithUndefined<{
|
|
52
|
-
localStorage: SelectFrom<Storage, {
|
|
53
|
-
setItem: true;
|
|
54
|
-
removeItem: true;
|
|
55
|
-
getItem: true;
|
|
56
|
-
}>;
|
|
57
|
-
}>;
|
|
58
|
-
}>;
|
|
59
|
-
/**
|
|
60
|
-
* An auth client for sending and validating client requests to a backend. This should only be used
|
|
61
|
-
* in a frontend environment as it accesses native browser APIs.
|
|
62
|
-
*
|
|
63
|
-
* @category Auth : Client
|
|
64
|
-
* @category Clients
|
|
65
|
-
*/
|
|
66
|
-
export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
|
|
67
|
-
protected readonly config: FrontendAuthClientConfig;
|
|
68
|
-
protected userCheckInterval: undefined | ReturnType<typeof createBlockingInterval>;
|
|
69
|
-
/** Used to clean up the activity listener on `.destroy()`. */
|
|
70
|
-
protected removeActivityListener: VoidFunction | undefined;
|
|
71
|
-
constructor(config: FrontendAuthClientConfig);
|
|
72
|
-
/**
|
|
73
|
-
* Destroys the client and performs all necessary cleanup (like clearing the user check
|
|
74
|
-
* interval).
|
|
75
|
-
*/
|
|
76
|
-
destroy(): void;
|
|
77
|
-
/**
|
|
78
|
-
* Assume the given user. Pass `undefined` to wipe the currently assumed user.
|
|
79
|
-
*
|
|
80
|
-
* @returns Whether the assumed user setting or clearing succeeded or not.
|
|
81
|
-
*/
|
|
82
|
-
assumeUser(assumedUserParams: Readonly<AssumedUserParams> | undefined): Promise<boolean>;
|
|
83
|
-
/** Gets the assumed user params stored in local storage, if any. */
|
|
84
|
-
getAssumedUser(): AssumedUserParams | undefined;
|
|
85
|
-
/**
|
|
86
|
-
* Creates a `RequestInit` object for the `fetch` API. If you have other request init options,
|
|
87
|
-
* use [`mergeDeep` from
|
|
88
|
-
* `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
|
|
89
|
-
* combine them with these.
|
|
90
|
-
*/
|
|
91
|
-
createAuthenticatedRequestInit(): RequestInit;
|
|
92
|
-
/** Wipes the current user auth. */
|
|
93
|
-
logout(): Promise<void>;
|
|
94
|
-
/**
|
|
95
|
-
* Use to handle a login response. The CSRF token cookie is automatically stored by the browser
|
|
96
|
-
* from the `Set-Cookie` response header.
|
|
97
|
-
*
|
|
98
|
-
* @throws Error if the login response failed.
|
|
99
|
-
*/
|
|
100
|
-
handleLoginResponse(response: Readonly<SelectFrom<Response, {
|
|
101
|
-
ok: true;
|
|
102
|
-
}>>): Promise<void>;
|
|
103
|
-
/**
|
|
104
|
-
* Use to verify _all_ responses received from the backend. Immediately logs the user out once
|
|
105
|
-
* an unauthorized response is detected.
|
|
106
|
-
*
|
|
107
|
-
* @returns `true` if the auth is okay, `false` otherwise.
|
|
108
|
-
*/
|
|
109
|
-
verifyResponseAuth(response: Readonly<PartialWithUndefined<SelectFrom<Response, {
|
|
110
|
-
status: true;
|
|
111
|
-
headers: true;
|
|
112
|
-
}>>>): Promise<boolean>;
|
|
113
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { HttpStatus, } from '@augment-vir/common';
|
|
2
|
-
import { listenToActivity } from 'detect-activity';
|
|
3
|
-
import { getCurrentCsrfToken, resolveCsrfHeaderName, } from '../csrf-token.js';
|
|
4
|
-
import { AuthHeaderName } from '../headers.js';
|
|
5
|
-
/**
|
|
6
|
-
* An auth client for sending and validating client requests to a backend. This should only be used
|
|
7
|
-
* in a frontend environment as it accesses native browser APIs.
|
|
8
|
-
*
|
|
9
|
-
* @category Auth : Client
|
|
10
|
-
* @category Clients
|
|
11
|
-
*/
|
|
12
|
-
export class FrontendAuthClient {
|
|
13
|
-
config;
|
|
14
|
-
userCheckInterval;
|
|
15
|
-
/** Used to clean up the activity listener on `.destroy()`. */
|
|
16
|
-
removeActivityListener;
|
|
17
|
-
constructor(config) {
|
|
18
|
-
this.config = config;
|
|
19
|
-
if (config.checkUser) {
|
|
20
|
-
this.removeActivityListener = listenToActivity({
|
|
21
|
-
listener: async () => {
|
|
22
|
-
const response = await config.checkUser?.performCheck();
|
|
23
|
-
if (response) {
|
|
24
|
-
await this.verifyResponseAuth({
|
|
25
|
-
status: response.status,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
debounce: config.checkUser.debounce || {
|
|
30
|
-
minutes: 1,
|
|
31
|
-
},
|
|
32
|
-
fireImmediately: false,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Destroys the client and performs all necessary cleanup (like clearing the user check
|
|
38
|
-
* interval).
|
|
39
|
-
*/
|
|
40
|
-
destroy() {
|
|
41
|
-
this.userCheckInterval?.clearInterval();
|
|
42
|
-
this.removeActivityListener?.();
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Assume the given user. Pass `undefined` to wipe the currently assumed user.
|
|
46
|
-
*
|
|
47
|
-
* @returns Whether the assumed user setting or clearing succeeded or not.
|
|
48
|
-
*/
|
|
49
|
-
async assumeUser(assumedUserParams) {
|
|
50
|
-
const localStorage = this.config.overrides?.localStorage || globalThis.localStorage;
|
|
51
|
-
const storageKey = this.config.assumedUserHeaderName || AuthHeaderName.AssumedUser;
|
|
52
|
-
if (!assumedUserParams) {
|
|
53
|
-
localStorage.removeItem(storageKey);
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
else if (!(await this.config.canAssumeUser?.())) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
localStorage.setItem(storageKey, JSON.stringify(assumedUserParams));
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
/** Gets the assumed user params stored in local storage, if any. */
|
|
63
|
-
getAssumedUser() {
|
|
64
|
-
const rawValue = (this.config.overrides?.localStorage || globalThis.localStorage).getItem(this.config.assumedUserHeaderName || AuthHeaderName.AssumedUser);
|
|
65
|
-
if (!rawValue) {
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
return JSON.parse(rawValue);
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Creates a `RequestInit` object for the `fetch` API. If you have other request init options,
|
|
77
|
-
* use [`mergeDeep` from
|
|
78
|
-
* `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
|
|
79
|
-
* combine them with these.
|
|
80
|
-
*/
|
|
81
|
-
createAuthenticatedRequestInit() {
|
|
82
|
-
const csrfToken = getCurrentCsrfToken();
|
|
83
|
-
const assumedUser = this.getAssumedUser();
|
|
84
|
-
const headers = {
|
|
85
|
-
...(csrfToken
|
|
86
|
-
? {
|
|
87
|
-
[resolveCsrfHeaderName(this.config.csrf)]: csrfToken,
|
|
88
|
-
}
|
|
89
|
-
: {}),
|
|
90
|
-
...(assumedUser
|
|
91
|
-
? {
|
|
92
|
-
[this.config.assumedUserHeaderName || AuthHeaderName.AssumedUser]: JSON.stringify(assumedUser),
|
|
93
|
-
}
|
|
94
|
-
: {}),
|
|
95
|
-
};
|
|
96
|
-
return {
|
|
97
|
-
headers,
|
|
98
|
-
credentials: 'include',
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
/** Wipes the current user auth. */
|
|
102
|
-
async logout() {
|
|
103
|
-
await this.config.authClearedCallback?.();
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Use to handle a login response. The CSRF token cookie is automatically stored by the browser
|
|
107
|
-
* from the `Set-Cookie` response header.
|
|
108
|
-
*
|
|
109
|
-
* @throws Error if the login response failed.
|
|
110
|
-
*/
|
|
111
|
-
async handleLoginResponse(response) {
|
|
112
|
-
if (!response.ok) {
|
|
113
|
-
await this.logout();
|
|
114
|
-
throw new Error('Login response failed.');
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Use to verify _all_ responses received from the backend. Immediately logs the user out once
|
|
119
|
-
* an unauthorized response is detected.
|
|
120
|
-
*
|
|
121
|
-
* @returns `true` if the auth is okay, `false` otherwise.
|
|
122
|
-
*/
|
|
123
|
-
async verifyResponseAuth(response) {
|
|
124
|
-
if (response.status === HttpStatus.Unauthorized &&
|
|
125
|
-
!response.headers?.get(AuthHeaderName.IsSignUpAuth)) {
|
|
126
|
-
await this.logout();
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { type AnyDuration, type FullDate, type UtcTimezone } from 'date-vir';
|
|
2
|
-
/**
|
|
3
|
-
* Determines if enough time has passed since the JWT was issued to start refreshing the session.
|
|
4
|
-
*
|
|
5
|
-
* Visually, this check looks like this:
|
|
6
|
-
*
|
|
7
|
-
* I====R===========E
|
|
8
|
-
*
|
|
9
|
-
* - I = JWT issued time (from the JWT's `iat` claim)
|
|
10
|
-
* - R = session refreshing is available now (I + sessionRefreshStartTime)
|
|
11
|
-
* - E = JWT expiration
|
|
12
|
-
* - `=` between R and E = the time frame in which the return value is `true`.
|
|
13
|
-
*
|
|
14
|
-
* @category Auth : Host
|
|
15
|
-
*/
|
|
16
|
-
export declare function isSessionRefreshReady({ now, jwtIssuedAt, sessionRefreshStartTime, }: {
|
|
17
|
-
/** The current time. */
|
|
18
|
-
now?: Readonly<FullDate<UtcTimezone>> | undefined;
|
|
19
|
-
/** When the JWT was issued (`iat` claim). */
|
|
20
|
-
jwtIssuedAt: Readonly<FullDate<UtcTimezone>>;
|
|
21
|
-
/** How long after JWT issuance before refreshing is available. */
|
|
22
|
-
sessionRefreshStartTime: Readonly<AnyDuration>;
|
|
23
|
-
}): boolean;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { calculateRelativeDate, getNowInUtcTimezone, isDateAfter, } from 'date-vir';
|
|
2
|
-
/**
|
|
3
|
-
* Determines if enough time has passed since the JWT was issued to start refreshing the session.
|
|
4
|
-
*
|
|
5
|
-
* Visually, this check looks like this:
|
|
6
|
-
*
|
|
7
|
-
* I====R===========E
|
|
8
|
-
*
|
|
9
|
-
* - I = JWT issued time (from the JWT's `iat` claim)
|
|
10
|
-
* - R = session refreshing is available now (I + sessionRefreshStartTime)
|
|
11
|
-
* - E = JWT expiration
|
|
12
|
-
* - `=` between R and E = the time frame in which the return value is `true`.
|
|
13
|
-
*
|
|
14
|
-
* @category Auth : Host
|
|
15
|
-
*/
|
|
16
|
-
export function isSessionRefreshReady({ now = getNowInUtcTimezone(), jwtIssuedAt, sessionRefreshStartTime, }) {
|
|
17
|
-
return isDateAfter({
|
|
18
|
-
fullDate: now,
|
|
19
|
-
relativeTo: calculateRelativeDate(jwtIssuedAt, sessionRefreshStartTime),
|
|
20
|
-
});
|
|
21
|
-
}
|