auth-vir 2.5.0 → 2.7.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/auth-client/backend-auth.client.d.ts +10 -4
- package/dist/auth-client/backend-auth.client.js +50 -6
- package/dist/auth-client/frontend-auth.client.d.ts +2 -0
- package/dist/auth-client/frontend-auth.client.js +14 -1
- package/dist/auth.d.ts +12 -1
- package/dist/auth.js +32 -3
- package/dist/csrf-token.js +6 -0
- package/dist/generated/browser.js +1 -0
- package/dist/generated/client.js +1 -0
- package/dist/generated/enums.js +1 -0
- package/dist/generated/internal/class.js +5 -4
- package/dist/generated/internal/prismaNamespace.d.ts +2 -2
- package/dist/generated/internal/prismaNamespace.js +5 -4
- package/dist/generated/internal/prismaNamespaceBrowser.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/jwt/jwt.d.ts +12 -0
- package/dist/jwt/jwt.js +22 -12
- package/dist/jwt/user-jwt.d.ts +6 -0
- package/dist/jwt/user-jwt.js +7 -1
- package/dist/log.d.ts +12 -0
- package/dist/log.js +17 -0
- package/package.json +11 -11
- package/src/auth-client/backend-auth.client.ts +86 -11
- package/src/auth-client/frontend-auth.client.ts +14 -1
- package/src/auth.ts +49 -2
- package/src/csrf-token.ts +6 -0
- package/src/generated/browser.ts +1 -0
- package/src/generated/client.ts +1 -0
- package/src/generated/commonInputTypes.ts +1 -0
- package/src/generated/enums.ts +1 -0
- package/src/generated/internal/class.ts +5 -4
- package/src/generated/internal/prismaNamespace.ts +5 -4
- package/src/generated/internal/prismaNamespaceBrowser.ts +1 -0
- package/src/generated/models/User.ts +1 -0
- package/src/generated/models.ts +1 -0
- package/src/index.ts +1 -0
- package/src/jwt/jwt.ts +26 -14
- package/src/jwt/user-jwt.ts +7 -1
- package/src/log.ts +18 -0
|
@@ -103,12 +103,18 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
|
|
|
103
103
|
*/
|
|
104
104
|
userSessionIdleTimeout: Readonly<AnyDuration>;
|
|
105
105
|
/**
|
|
106
|
-
* How long
|
|
107
|
-
* session.
|
|
106
|
+
* How long into a user's session when we should start trying to refresh their session.
|
|
108
107
|
*
|
|
109
|
-
* @default {minutes:
|
|
108
|
+
* @default {minutes: 2}
|
|
110
109
|
*/
|
|
111
|
-
|
|
110
|
+
sessionRefreshTimeout: Readonly<AnyDuration>;
|
|
111
|
+
/**
|
|
112
|
+
* The maximum duration a session can last, regardless of activity. After this time, the
|
|
113
|
+
* user will be logged out even if they are actively using the application.
|
|
114
|
+
*
|
|
115
|
+
* @default {weeks: 2}
|
|
116
|
+
*/
|
|
117
|
+
maxSessionDuration: Readonly<AnyDuration>;
|
|
112
118
|
overrides: PartialWithUndefined<{
|
|
113
119
|
csrfHeaderName: CsrfHeaderName;
|
|
114
120
|
assumedUserHeaderName: string;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { ensureArray, } from '@augment-vir/common';
|
|
2
|
-
import { calculateRelativeDate, getNowInUtcTimezone, isDateAfter } from 'date-vir';
|
|
2
|
+
import { calculateRelativeDate, createUtcFullDate, getNowInUtcTimezone, isDateAfter, negateDuration, } from 'date-vir';
|
|
3
3
|
import { extractUserIdFromRequestHeaders, generateLogoutHeaders, generateSuccessfulLoginHeaders, insecureExtractUserIdFromCookieAlone, } from '../auth.js';
|
|
4
4
|
import { AuthCookieName } from '../cookie.js';
|
|
5
5
|
import { AuthHeaderName, mergeHeaderValues } from '../headers.js';
|
|
6
6
|
import { parseJwtKeys } from '../jwt/jwt-keys.js';
|
|
7
|
+
import { authLog } from '../log.js';
|
|
7
8
|
const defaultSessionIdleTimeout = {
|
|
8
9
|
minutes: 20,
|
|
9
10
|
};
|
|
10
|
-
const
|
|
11
|
-
minutes:
|
|
11
|
+
const defaultSessionRefreshTimeout = {
|
|
12
|
+
minutes: 2,
|
|
13
|
+
};
|
|
14
|
+
const defaultMaxSessionDuration = {
|
|
15
|
+
weeks: 2,
|
|
12
16
|
};
|
|
13
17
|
/**
|
|
14
18
|
* An auth client for creating and validating JWTs embedded in cookies. This should only be used in
|
|
@@ -60,8 +64,33 @@ export class BackendAuthClient {
|
|
|
60
64
|
relativeTo: userIdResult.jwtExpiration,
|
|
61
65
|
});
|
|
62
66
|
if (isExpiredAlready) {
|
|
67
|
+
authLog('auth-vir: SESSION EXPIRED - JWT already expired, user will be logged out', {
|
|
68
|
+
userId: userIdResult.userId,
|
|
69
|
+
jwtExpiration: userIdResult.jwtExpiration,
|
|
70
|
+
});
|
|
63
71
|
return undefined;
|
|
64
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Check if the session has exceeded the max session duration. If so, don't refresh the
|
|
75
|
+
* session and let it expire naturally.
|
|
76
|
+
*/
|
|
77
|
+
const maxSessionDuration = this.config.maxSessionDuration || defaultMaxSessionDuration;
|
|
78
|
+
if (userIdResult.sessionStartedAt) {
|
|
79
|
+
const sessionStartDate = createUtcFullDate(userIdResult.sessionStartedAt);
|
|
80
|
+
const maxSessionEndDate = calculateRelativeDate(sessionStartDate, maxSessionDuration);
|
|
81
|
+
const isSessionExpired = isDateAfter({
|
|
82
|
+
fullDate: now,
|
|
83
|
+
relativeTo: maxSessionEndDate,
|
|
84
|
+
});
|
|
85
|
+
if (isSessionExpired) {
|
|
86
|
+
authLog('auth-vir: SESSION EXPIRED - max session duration exceeded, user will be logged out', {
|
|
87
|
+
userId: userIdResult.userId,
|
|
88
|
+
sessionStartedAt: userIdResult.sessionStartedAt,
|
|
89
|
+
maxSessionDuration,
|
|
90
|
+
});
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
65
94
|
/**
|
|
66
95
|
* This check performs the following: the current time + the refresh threshold > JWT
|
|
67
96
|
* expiration.
|
|
@@ -77,9 +106,10 @@ export class BackendAuthClient {
|
|
|
77
106
|
* - Y = JWT expiration within the refresh threshold: {@link isRefreshReady} = true.
|
|
78
107
|
* - Z = JWT expiration outside the refresh threshold: {@link isRefreshReady} = false.
|
|
79
108
|
*/
|
|
109
|
+
const sessionRefreshTimeout = this.config.sessionRefreshTimeout || defaultSessionRefreshTimeout;
|
|
80
110
|
const isRefreshReady = isDateAfter({
|
|
81
|
-
fullDate:
|
|
82
|
-
relativeTo: userIdResult.jwtExpiration,
|
|
111
|
+
fullDate: now,
|
|
112
|
+
relativeTo: calculateRelativeDate(userIdResult.jwtExpiration, negateDuration(sessionRefreshTimeout)),
|
|
83
113
|
});
|
|
84
114
|
if (isRefreshReady) {
|
|
85
115
|
return this.createLoginHeaders({
|
|
@@ -116,6 +146,7 @@ export class BackendAuthClient {
|
|
|
116
146
|
async getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }) {
|
|
117
147
|
const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth, this.config.overrides);
|
|
118
148
|
if (!userIdResult) {
|
|
149
|
+
authLog('auth-vir: getSecureUser failed - could not extract user from request');
|
|
119
150
|
return undefined;
|
|
120
151
|
}
|
|
121
152
|
const user = await this.getDatabaseUser({
|
|
@@ -124,6 +155,9 @@ export class BackendAuthClient {
|
|
|
124
155
|
isSignUpCookie,
|
|
125
156
|
});
|
|
126
157
|
if (!user) {
|
|
158
|
+
authLog('auth-vir: getSecureUser failed - user not found in database', {
|
|
159
|
+
userId: userIdResult.userId,
|
|
160
|
+
});
|
|
127
161
|
return undefined;
|
|
128
162
|
}
|
|
129
163
|
const assumedUser = await this.getAssumedUser({
|
|
@@ -161,6 +195,10 @@ export class BackendAuthClient {
|
|
|
161
195
|
}
|
|
162
196
|
/** Use these headers to log out the user. */
|
|
163
197
|
async createLogoutHeaders(params) {
|
|
198
|
+
authLog('auth-vir: LOGOUT - BackendAuthClient.createLogoutHeaders called', {
|
|
199
|
+
allCookies: 'allCookies' in params ? params.allCookies : undefined,
|
|
200
|
+
isSignUpCookie: 'isSignUpCookie' in params ? params.isSignUpCookie : undefined,
|
|
201
|
+
}, new Error().stack);
|
|
164
202
|
const signUpCookieHeaders = params.allCookies || params.isSignUpCookie
|
|
165
203
|
? generateLogoutHeaders(await this.getCookieParams({
|
|
166
204
|
isSignUpCookie: true,
|
|
@@ -195,10 +233,12 @@ export class BackendAuthClient {
|
|
|
195
233
|
requestHeaders,
|
|
196
234
|
}), this.config.overrides)
|
|
197
235
|
: undefined;
|
|
236
|
+
const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth, this.config.overrides);
|
|
237
|
+
const sessionStartedAt = existingUserIdResult?.sessionStartedAt;
|
|
198
238
|
const newCookieHeaders = await generateSuccessfulLoginHeaders(userId, await this.getCookieParams({
|
|
199
239
|
isSignUpCookie,
|
|
200
240
|
requestHeaders,
|
|
201
|
-
}), this.config.overrides);
|
|
241
|
+
}), this.config.overrides, sessionStartedAt);
|
|
202
242
|
return {
|
|
203
243
|
...newCookieHeaders,
|
|
204
244
|
'set-cookie': mergeHeaderValues(newCookieHeaders['set-cookie'], discardOppositeCookieHeaders?.['set-cookie']),
|
|
@@ -228,6 +268,7 @@ export class BackendAuthClient {
|
|
|
228
268
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
229
269
|
const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookieName.Auth);
|
|
230
270
|
if (!userIdResult) {
|
|
271
|
+
authLog('auth-vir: getInsecureUser failed - could not extract user from request');
|
|
231
272
|
return undefined;
|
|
232
273
|
}
|
|
233
274
|
const user = await this.getDatabaseUser({
|
|
@@ -236,6 +277,9 @@ export class BackendAuthClient {
|
|
|
236
277
|
assumingUser: undefined,
|
|
237
278
|
});
|
|
238
279
|
if (!user) {
|
|
280
|
+
authLog('auth-vir: getInsecureUser failed - user not found in database', {
|
|
281
|
+
userId: userIdResult.userId,
|
|
282
|
+
});
|
|
239
283
|
return undefined;
|
|
240
284
|
}
|
|
241
285
|
const refreshHeaders = allowUserAuthRefresh &&
|
|
@@ -56,6 +56,8 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
|
|
|
56
56
|
export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
|
|
57
57
|
protected readonly config: FrontendAuthClientConfig;
|
|
58
58
|
protected userCheckInterval: undefined | ReturnType<typeof createBlockingInterval>;
|
|
59
|
+
/** Used to clean up the activity listener on `.destroy()`. */
|
|
60
|
+
protected removeActivityListener: VoidFunction | undefined;
|
|
59
61
|
constructor(config?: FrontendAuthClientConfig);
|
|
60
62
|
/**
|
|
61
63
|
* Destroys the client and performs all necessary cleanup (like clearing the user check
|
|
@@ -2,6 +2,7 @@ import { HttpStatus, } from '@augment-vir/common';
|
|
|
2
2
|
import { listenToActivity } from 'detect-activity';
|
|
3
3
|
import { CsrfTokenFailureReason, extractCsrfTokenHeader, getCurrentCsrfToken, storeCsrfToken, wipeCurrentCsrfToken, } from '../csrf-token.js';
|
|
4
4
|
import { AuthHeaderName } from '../headers.js';
|
|
5
|
+
import { authLog } from '../log.js';
|
|
5
6
|
/**
|
|
6
7
|
* An auth client for sending and validating client requests to a backend. This should only be used
|
|
7
8
|
* in a frontend environment as it accesses native browser APIs.
|
|
@@ -12,10 +13,12 @@ import { AuthHeaderName } from '../headers.js';
|
|
|
12
13
|
export class FrontendAuthClient {
|
|
13
14
|
config;
|
|
14
15
|
userCheckInterval;
|
|
16
|
+
/** Used to clean up the activity listener on `.destroy()`. */
|
|
17
|
+
removeActivityListener;
|
|
15
18
|
constructor(config = {}) {
|
|
16
19
|
this.config = config;
|
|
17
20
|
if (config.checkUser) {
|
|
18
|
-
listenToActivity({
|
|
21
|
+
this.removeActivityListener = listenToActivity({
|
|
19
22
|
listener: async () => {
|
|
20
23
|
const response = await config.checkUser?.performCheck();
|
|
21
24
|
if (response) {
|
|
@@ -35,12 +38,16 @@ export class FrontendAuthClient {
|
|
|
35
38
|
*/
|
|
36
39
|
destroy() {
|
|
37
40
|
this.userCheckInterval?.clearInterval();
|
|
41
|
+
this.removeActivityListener?.();
|
|
38
42
|
}
|
|
39
43
|
/** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
|
|
40
44
|
async getCurrentCsrfToken() {
|
|
41
45
|
const csrfTokenResult = getCurrentCsrfToken(this.config.overrides);
|
|
42
46
|
if (csrfTokenResult.failure &&
|
|
43
47
|
csrfTokenResult.failure !== CsrfTokenFailureReason.DoesNotExist) {
|
|
48
|
+
authLog('auth-vir: LOGOUT - getCurrentCsrfToken: invalid CSRF token', {
|
|
49
|
+
failure: csrfTokenResult.failure,
|
|
50
|
+
});
|
|
44
51
|
await this.logout();
|
|
45
52
|
return undefined;
|
|
46
53
|
}
|
|
@@ -107,6 +114,7 @@ export class FrontendAuthClient {
|
|
|
107
114
|
}
|
|
108
115
|
/** Wipes the current user auth. */
|
|
109
116
|
async logout() {
|
|
117
|
+
authLog('auth-vir: LOGOUT - FrontendAuthClient.logout called', new Error().stack);
|
|
110
118
|
await this.config.authClearedCallback?.();
|
|
111
119
|
wipeCurrentCsrfToken(this.config.overrides);
|
|
112
120
|
}
|
|
@@ -118,11 +126,13 @@ export class FrontendAuthClient {
|
|
|
118
126
|
*/
|
|
119
127
|
async handleLoginResponse(response) {
|
|
120
128
|
if (!response.ok) {
|
|
129
|
+
authLog('auth-vir: LOGOUT - handleLoginResponse: response not ok');
|
|
121
130
|
await this.logout();
|
|
122
131
|
throw new Error('Login response failed.');
|
|
123
132
|
}
|
|
124
133
|
const { csrfToken } = extractCsrfTokenHeader(response, this.config.overrides);
|
|
125
134
|
if (!csrfToken) {
|
|
135
|
+
authLog('auth-vir: LOGOUT - handleLoginResponse: no CSRF token in response');
|
|
126
136
|
await this.logout();
|
|
127
137
|
throw new Error('Did not receive any CSRF token.');
|
|
128
138
|
}
|
|
@@ -137,6 +147,9 @@ export class FrontendAuthClient {
|
|
|
137
147
|
async verifyResponseAuth(response) {
|
|
138
148
|
if (response.status === HttpStatus.Unauthorized &&
|
|
139
149
|
!response.headers?.get(AuthHeaderName.IsSignUpAuth)) {
|
|
150
|
+
authLog('auth-vir: LOGOUT - verifyResponseAuth: unauthorized response (401)', {
|
|
151
|
+
status: response.status,
|
|
152
|
+
});
|
|
140
153
|
await this.logout();
|
|
141
154
|
return false;
|
|
142
155
|
}
|
package/dist/auth.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type FullDate, type UtcTimezone } from 'date-vir';
|
|
|
3
3
|
import { type CookieParams } from './cookie.js';
|
|
4
4
|
import { AuthHeaderName } from './headers.js';
|
|
5
5
|
import { type ParseJwtParams } from './jwt/jwt.js';
|
|
6
|
+
import { type JwtUserData } from './jwt/user-jwt.js';
|
|
6
7
|
/**
|
|
7
8
|
* All possible headers container types supported by {@link extractUserIdFromRequestHeaders}.
|
|
8
9
|
*
|
|
@@ -18,6 +19,11 @@ export type UserIdResult<UserId extends string | number> = {
|
|
|
18
19
|
userId: UserId;
|
|
19
20
|
jwtExpiration: FullDate<UtcTimezone>;
|
|
20
21
|
cookieName: string;
|
|
22
|
+
/**
|
|
23
|
+
* Unix timestamp (in milliseconds) when the session was originally started. Used to enforce max
|
|
24
|
+
* session duration.
|
|
25
|
+
*/
|
|
26
|
+
sessionStartedAt: JwtUserData['sessionStartedAt'];
|
|
21
27
|
};
|
|
22
28
|
/**
|
|
23
29
|
* Extract the user id from a request by checking both the request cookie and CSRF token. This is
|
|
@@ -48,7 +54,12 @@ export declare function generateSuccessfulLoginHeaders<CsrfHeaderName extends st
|
|
|
48
54
|
/** The id from your database of the user you're authenticating. */
|
|
49
55
|
userId: string | number, cookieConfig: Readonly<CookieParams>, overrides?: PartialWithUndefined<{
|
|
50
56
|
csrfHeaderName: CsrfHeaderName;
|
|
51
|
-
}
|
|
57
|
+
}>,
|
|
58
|
+
/**
|
|
59
|
+
* The timestamp (in seconds) when the session originally started. If not provided, the current
|
|
60
|
+
* time will be used (for new sessions).
|
|
61
|
+
*/
|
|
62
|
+
sessionStartedAt?: number | undefined): Promise<{
|
|
52
63
|
'set-cookie': string;
|
|
53
64
|
} & Record<CsrfHeaderName, string>>;
|
|
54
65
|
/**
|
package/dist/auth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AuthCookieName, clearAuthCookie, extractCookieJwt, generateAuthCookie, } from './cookie.js';
|
|
2
2
|
import { extractCsrfTokenHeader, generateCsrfToken, parseCsrfToken, storeCsrfToken, wipeCurrentCsrfToken, } from './csrf-token.js';
|
|
3
3
|
import { AuthHeaderName } from './headers.js';
|
|
4
|
+
import { authLog } from './log.js';
|
|
4
5
|
function readHeader(headers, headerName) {
|
|
5
6
|
if (headers instanceof Headers) {
|
|
6
7
|
return headers.get(headerName) || undefined;
|
|
@@ -38,19 +39,31 @@ export async function extractUserIdFromRequestHeaders(headers, jwtParams, cookie
|
|
|
38
39
|
const csrfToken = readCsrfTokenHeader(headers, overrides);
|
|
39
40
|
const cookie = readHeader(headers, 'cookie');
|
|
40
41
|
if (!cookie || !csrfToken) {
|
|
42
|
+
authLog('auth-vir: extractUserIdFromRequestHeaders failed - missing cookie or CSRF token', {
|
|
43
|
+
hasCookie: !!cookie,
|
|
44
|
+
hasCsrfToken: !!csrfToken,
|
|
45
|
+
cookieName,
|
|
46
|
+
});
|
|
41
47
|
return undefined;
|
|
42
48
|
}
|
|
43
49
|
const jwt = await extractCookieJwt(cookie, jwtParams, cookieName);
|
|
44
50
|
if (!jwt || jwt.data.csrfToken !== csrfToken) {
|
|
51
|
+
authLog('auth-vir: extractUserIdFromRequestHeaders failed - JWT invalid or CSRF mismatch', {
|
|
52
|
+
hasJwt: !!jwt,
|
|
53
|
+
csrfMatch: jwt ? jwt.data.csrfToken === csrfToken : false,
|
|
54
|
+
cookieName,
|
|
55
|
+
});
|
|
45
56
|
return undefined;
|
|
46
57
|
}
|
|
47
58
|
return {
|
|
48
59
|
userId: jwt.data.userId,
|
|
49
60
|
jwtExpiration: jwt.jwtExpiration,
|
|
50
61
|
cookieName,
|
|
62
|
+
sessionStartedAt: jwt.data.sessionStartedAt,
|
|
51
63
|
};
|
|
52
64
|
}
|
|
53
|
-
catch {
|
|
65
|
+
catch (error) {
|
|
66
|
+
authLog('auth-vir: extractUserIdFromRequestHeaders error', { error, cookieName });
|
|
54
67
|
return undefined;
|
|
55
68
|
}
|
|
56
69
|
}
|
|
@@ -66,19 +79,23 @@ export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, c
|
|
|
66
79
|
try {
|
|
67
80
|
const cookie = readHeader(headers, 'cookie');
|
|
68
81
|
if (!cookie) {
|
|
82
|
+
authLog('auth-vir: insecureExtractUserIdFromCookieAlone failed - no cookie');
|
|
69
83
|
return undefined;
|
|
70
84
|
}
|
|
71
85
|
const jwt = await extractCookieJwt(cookie, jwtParams, cookieName);
|
|
72
86
|
if (!jwt) {
|
|
87
|
+
authLog('auth-vir: insecureExtractUserIdFromCookieAlone failed - JWT extraction failed');
|
|
73
88
|
return undefined;
|
|
74
89
|
}
|
|
75
90
|
return {
|
|
76
91
|
userId: jwt.data.userId,
|
|
77
92
|
jwtExpiration: jwt.jwtExpiration,
|
|
78
93
|
cookieName,
|
|
94
|
+
sessionStartedAt: jwt.data.sessionStartedAt,
|
|
79
95
|
};
|
|
80
96
|
}
|
|
81
|
-
catch {
|
|
97
|
+
catch (error) {
|
|
98
|
+
authLog('auth-vir: insecureExtractUserIdFromCookieAlone error', { error });
|
|
82
99
|
return undefined;
|
|
83
100
|
}
|
|
84
101
|
}
|
|
@@ -89,13 +106,19 @@ export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, c
|
|
|
89
106
|
*/
|
|
90
107
|
export async function generateSuccessfulLoginHeaders(
|
|
91
108
|
/** The id from your database of the user you're authenticating. */
|
|
92
|
-
userId, cookieConfig, overrides = {}
|
|
109
|
+
userId, cookieConfig, overrides = {},
|
|
110
|
+
/**
|
|
111
|
+
* The timestamp (in seconds) when the session originally started. If not provided, the current
|
|
112
|
+
* time will be used (for new sessions).
|
|
113
|
+
*/
|
|
114
|
+
sessionStartedAt) {
|
|
93
115
|
const csrfToken = generateCsrfToken(cookieConfig.cookieDuration);
|
|
94
116
|
const csrfHeaderName = (overrides.csrfHeaderName || AuthHeaderName.CsrfToken);
|
|
95
117
|
return {
|
|
96
118
|
'set-cookie': await generateAuthCookie({
|
|
97
119
|
csrfToken: csrfToken.token,
|
|
98
120
|
userId,
|
|
121
|
+
sessionStartedAt: sessionStartedAt ?? Date.now(),
|
|
99
122
|
}, cookieConfig),
|
|
100
123
|
[csrfHeaderName]: JSON.stringify(csrfToken),
|
|
101
124
|
};
|
|
@@ -107,6 +130,9 @@ userId, cookieConfig, overrides = {}) {
|
|
|
107
130
|
* @category Auth : Host
|
|
108
131
|
*/
|
|
109
132
|
export function generateLogoutHeaders(cookieConfig, overrides = {}) {
|
|
133
|
+
authLog('auth-vir: LOGOUT - generateLogoutHeaders called', {
|
|
134
|
+
cookieName: cookieConfig.cookieName,
|
|
135
|
+
}, new Error().stack);
|
|
110
136
|
const csrfHeaderName = (overrides.csrfHeaderName || AuthHeaderName.CsrfToken);
|
|
111
137
|
return {
|
|
112
138
|
'set-cookie': clearAuthCookie(cookieConfig),
|
|
@@ -124,13 +150,16 @@ export function generateLogoutHeaders(cookieConfig, overrides = {}) {
|
|
|
124
150
|
*/
|
|
125
151
|
export function handleAuthResponse(response, overrides = {}) {
|
|
126
152
|
if (!response.ok) {
|
|
153
|
+
authLog('auth-vir: LOGOUT - handleAuthResponse: response not ok, wiping CSRF token');
|
|
127
154
|
wipeCurrentCsrfToken(overrides);
|
|
128
155
|
return;
|
|
129
156
|
}
|
|
130
157
|
const { csrfToken } = extractCsrfTokenHeader(response, overrides);
|
|
131
158
|
if (!csrfToken) {
|
|
159
|
+
authLog('auth-vir: LOGOUT - handleAuthResponse: no CSRF token in response, wiping');
|
|
132
160
|
wipeCurrentCsrfToken(overrides);
|
|
133
161
|
throw new Error('Did not receive any CSRF token.');
|
|
134
162
|
}
|
|
163
|
+
authLog('auth-vir: handleAuthResponse - successfully stored CSRF token');
|
|
135
164
|
storeCsrfToken(csrfToken, overrides);
|
|
136
165
|
}
|
package/dist/csrf-token.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomString, wrapInTry, } from '@augment-vir/common';
|
|
|
2
2
|
import { calculateRelativeDate, fullDateShape, getNowInUtcTimezone, isDateAfter, } from 'date-vir';
|
|
3
3
|
import { defineShape, parseJsonWithShape } from 'object-shape-tester';
|
|
4
4
|
import { AuthHeaderName } from './headers.js';
|
|
5
|
+
import { authLog } from './log.js';
|
|
5
6
|
/**
|
|
6
7
|
* Shape definition for {@link CsrfToken}.
|
|
7
8
|
*
|
|
@@ -74,6 +75,7 @@ export function parseCsrfToken(value) {
|
|
|
74
75
|
fallbackValue: undefined,
|
|
75
76
|
});
|
|
76
77
|
if (!csrfToken) {
|
|
78
|
+
authLog('auth-vir: CSRF token parse failed - will cause logout if used');
|
|
77
79
|
return {
|
|
78
80
|
failure: CsrfTokenFailureReason.ParseFailed,
|
|
79
81
|
};
|
|
@@ -82,6 +84,9 @@ export function parseCsrfToken(value) {
|
|
|
82
84
|
fullDate: getNowInUtcTimezone(),
|
|
83
85
|
relativeTo: csrfToken.expiration,
|
|
84
86
|
})) {
|
|
87
|
+
authLog('auth-vir: CSRF token expired - will cause logout', {
|
|
88
|
+
expiration: csrfToken.expiration,
|
|
89
|
+
});
|
|
85
90
|
return {
|
|
86
91
|
failure: CsrfTokenFailureReason.Expired,
|
|
87
92
|
};
|
|
@@ -107,5 +112,6 @@ export function getCurrentCsrfToken(overrides = {}) {
|
|
|
107
112
|
* @category Auth : Client
|
|
108
113
|
*/
|
|
109
114
|
export function wipeCurrentCsrfToken(overrides = {}) {
|
|
115
|
+
authLog('auth-vir: wipeCurrentCsrfToken called', new Error().stack);
|
|
110
116
|
return (overrides.localStorage || globalThis.localStorage).removeItem(overrides.csrfHeaderName || AuthHeaderName.CsrfToken);
|
|
111
117
|
}
|
package/dist/generated/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
|
2
2
|
/* eslint-disable */
|
|
3
|
+
// biome-ignore-all lint: generated file
|
|
3
4
|
// @ts-nocheck
|
|
4
5
|
/*
|
|
5
6
|
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
|
package/dist/generated/enums.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
|
2
2
|
/* eslint-disable */
|
|
3
|
+
// biome-ignore-all lint: generated file
|
|
3
4
|
// @ts-nocheck
|
|
4
5
|
/*
|
|
5
6
|
* WARNING: This is an internal file that is subject to change!
|
|
@@ -21,8 +22,8 @@ const config = {
|
|
|
21
22
|
"fromEnvVar": null
|
|
22
23
|
},
|
|
23
24
|
"config": {
|
|
24
|
-
"
|
|
25
|
-
"
|
|
25
|
+
"engineType": "client",
|
|
26
|
+
"moduleFormat": "esm"
|
|
26
27
|
},
|
|
27
28
|
"binaryTargets": [
|
|
28
29
|
{
|
|
@@ -39,8 +40,8 @@ const config = {
|
|
|
39
40
|
"isCustomOutput": true
|
|
40
41
|
},
|
|
41
42
|
"relativePath": "../../test-files",
|
|
42
|
-
"clientVersion": "6.
|
|
43
|
-
"engineVersion": "
|
|
43
|
+
"clientVersion": "6.19.2",
|
|
44
|
+
"engineVersion": "c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
|
44
45
|
"datasourceNames": [
|
|
45
46
|
"db"
|
|
46
47
|
],
|
|
@@ -58,8 +58,8 @@ export type PrismaVersion = {
|
|
|
58
58
|
engine: string;
|
|
59
59
|
};
|
|
60
60
|
/**
|
|
61
|
-
* Prisma Client JS version: 6.
|
|
62
|
-
* Query Engine version:
|
|
61
|
+
* Prisma Client JS version: 6.19.2
|
|
62
|
+
* Query Engine version: c2990dca591cba766e3b7ef5d9e8a84796e47ab7
|
|
63
63
|
*/
|
|
64
64
|
export declare const prismaVersion: PrismaVersion;
|
|
65
65
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
|
2
2
|
/* eslint-disable */
|
|
3
|
+
// biome-ignore-all lint: generated file
|
|
3
4
|
/*
|
|
4
5
|
* WARNING: This is an internal file that is subject to change!
|
|
5
6
|
*
|
|
@@ -38,12 +39,12 @@ export const skip = runtime.skip;
|
|
|
38
39
|
export const Decimal = runtime.Decimal;
|
|
39
40
|
export const getExtensionContext = runtime.Extensions.getExtensionContext;
|
|
40
41
|
/**
|
|
41
|
-
* Prisma Client JS version: 6.
|
|
42
|
-
* Query Engine version:
|
|
42
|
+
* Prisma Client JS version: 6.19.2
|
|
43
|
+
* Query Engine version: c2990dca591cba766e3b7ef5d9e8a84796e47ab7
|
|
43
44
|
*/
|
|
44
45
|
export const prismaVersion = {
|
|
45
|
-
client: "6.
|
|
46
|
-
engine: "
|
|
46
|
+
client: "6.19.2",
|
|
47
|
+
engine: "c2990dca591cba766e3b7ef5d9e8a84796e47ab7"
|
|
47
48
|
};
|
|
48
49
|
export const NullTypes = {
|
|
49
50
|
DbNull: runtime.objectEnumValues.classes.DbNull,
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/jwt/jwt.d.ts
CHANGED
|
@@ -60,6 +60,18 @@ export type CreateJwtParams = Readonly<{
|
|
|
60
60
|
*/
|
|
61
61
|
notValidUntil: DateLike;
|
|
62
62
|
}>>;
|
|
63
|
+
/**
|
|
64
|
+
* JWT uses seconds since the epoch per RFC 7519, whereas `toTimestamp` uses milliseconds.
|
|
65
|
+
*
|
|
66
|
+
* @category Internal
|
|
67
|
+
*/
|
|
68
|
+
export declare function toJwtTimestamp(date: Readonly<FullDate>): number;
|
|
69
|
+
/**
|
|
70
|
+
* Converts a JWT timestamp (in seconds) into a FullDate instance.
|
|
71
|
+
*
|
|
72
|
+
* @category Internal
|
|
73
|
+
*/
|
|
74
|
+
export declare function parseJwtTimestamp(seconds: number): FullDate<UtcTimezone>;
|
|
63
75
|
/**
|
|
64
76
|
* Creates a signed and encrypted JWT that contains the given data.
|
|
65
77
|
*
|
package/dist/jwt/jwt.js
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import { assertWrap, check } from '@augment-vir/assert';
|
|
2
|
-
import { calculateRelativeDate, createFullDateInUserTimezone, createUtcFullDate, getNowInUtcTimezone,
|
|
2
|
+
import { calculateRelativeDate, createFullDateInUserTimezone, createUtcFullDate, getNowInUtcTimezone, toTimestamp, } from 'date-vir';
|
|
3
3
|
import { EncryptJWT, jwtDecrypt, jwtVerify, SignJWT } from 'jose';
|
|
4
4
|
const encryptionProtectedHeader = { alg: 'dir', enc: 'A256GCM' };
|
|
5
5
|
const signingProtectedHeader = { alg: 'HS512' };
|
|
6
|
+
/**
|
|
7
|
+
* JWT uses seconds since the epoch per RFC 7519, whereas `toTimestamp` uses milliseconds.
|
|
8
|
+
*
|
|
9
|
+
* @category Internal
|
|
10
|
+
*/
|
|
11
|
+
export function toJwtTimestamp(date) {
|
|
12
|
+
return Math.floor(toTimestamp(date) / 1000);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Converts a JWT timestamp (in seconds) into a FullDate instance.
|
|
16
|
+
*
|
|
17
|
+
* @category Internal
|
|
18
|
+
*/
|
|
19
|
+
export function parseJwtTimestamp(seconds) {
|
|
20
|
+
return createUtcFullDate(seconds * 1000);
|
|
21
|
+
}
|
|
6
22
|
/**
|
|
7
23
|
* Creates a signed and encrypted JWT that contains the given data.
|
|
8
24
|
*
|
|
@@ -14,13 +30,13 @@ data, params) {
|
|
|
14
30
|
const rawJwt = new SignJWT({ data })
|
|
15
31
|
.setProtectedHeader(signingProtectedHeader)
|
|
16
32
|
.setIssuedAt(params.issuedAt
|
|
17
|
-
?
|
|
33
|
+
? toJwtTimestamp(createFullDateInUserTimezone(params.issuedAt))
|
|
18
34
|
: undefined)
|
|
19
35
|
.setIssuer(params.issuer)
|
|
20
36
|
.setAudience(params.audience)
|
|
21
|
-
.setExpirationTime(
|
|
37
|
+
.setExpirationTime(toJwtTimestamp(calculateRelativeDate(getNowInUtcTimezone(), params.jwtDuration)));
|
|
22
38
|
if (params.notValidUntil) {
|
|
23
|
-
rawJwt.setNotBefore(
|
|
39
|
+
rawJwt.setNotBefore(toJwtTimestamp(createFullDateInUserTimezone(params.notValidUntil)));
|
|
24
40
|
}
|
|
25
41
|
const signedJwt = await rawJwt.sign(params.jwtKeys.signingKey);
|
|
26
42
|
return await new EncryptJWT({ jwt: signedJwt })
|
|
@@ -57,14 +73,8 @@ export async function parseJwt(encryptedJwt, params) {
|
|
|
57
73
|
if (!check.deepEquals(verifiedJwt.protectedHeader, signingProtectedHeader)) {
|
|
58
74
|
throw new Error('Invalid signing protected header.');
|
|
59
75
|
}
|
|
60
|
-
const
|
|
61
|
-
const jwtExpiration =
|
|
62
|
-
if (isDateAfter({
|
|
63
|
-
fullDate: getNowInUtcTimezone(),
|
|
64
|
-
relativeTo: jwtExpiration,
|
|
65
|
-
})) {
|
|
66
|
-
throw new Error('JWT expired.');
|
|
67
|
-
}
|
|
76
|
+
const expirationSeconds = assertWrap.isDefined(verifiedJwt.payload.exp, 'JWT has no expiration.');
|
|
77
|
+
const jwtExpiration = parseJwtTimestamp(expirationSeconds);
|
|
68
78
|
return {
|
|
69
79
|
data: data,
|
|
70
80
|
jwtExpiration,
|
package/dist/jwt/user-jwt.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ export declare const userJwtDataShape: import("object-shape-tester").Shape<{
|
|
|
13
13
|
* Consider using {@link generateCsrfToken} to generate this.
|
|
14
14
|
*/
|
|
15
15
|
csrfToken: string;
|
|
16
|
+
/**
|
|
17
|
+
* Unix timestamp (in milliseconds) when the session was originally started. This is used to
|
|
18
|
+
* enforce the max session duration. If not present, the session is considered to have started
|
|
19
|
+
* when the JWT was issued.
|
|
20
|
+
*/
|
|
21
|
+
sessionStartedAt: import("object-shape-tester").Shape<import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TUndefined, import("@sinclair/typebox").TNumber]>>>;
|
|
16
22
|
}>;
|
|
17
23
|
/**
|
|
18
24
|
* Data required for user JWTs.
|
package/dist/jwt/user-jwt.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { checkValidShape, defineShape, unionShape } from 'object-shape-tester';
|
|
1
|
+
import { checkValidShape, defineShape, optionalShape, unionShape } from 'object-shape-tester';
|
|
2
2
|
import { createJwt, parseJwt, } from './jwt.js';
|
|
3
3
|
/**
|
|
4
4
|
* Shape definition and source of truth for {@link JwtUserData}.
|
|
@@ -14,6 +14,12 @@ export const userJwtDataShape = defineShape({
|
|
|
14
14
|
* Consider using {@link generateCsrfToken} to generate this.
|
|
15
15
|
*/
|
|
16
16
|
csrfToken: '',
|
|
17
|
+
/**
|
|
18
|
+
* Unix timestamp (in milliseconds) when the session was originally started. This is used to
|
|
19
|
+
* enforce the max session duration. If not present, the session is considered to have started
|
|
20
|
+
* when the JWT was issued.
|
|
21
|
+
*/
|
|
22
|
+
sessionStartedAt: optionalShape(0, { alsoUndefined: true }),
|
|
17
23
|
});
|
|
18
24
|
/**
|
|
19
25
|
* Creates a new signed and encrypted {@link JwtUserData} when a client (frontend) successfully
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send logs to the console for debugging.
|
|
3
|
+
*
|
|
4
|
+
* @category Internal
|
|
5
|
+
*/
|
|
6
|
+
export declare function authLog(...params: any[]): void;
|
|
7
|
+
/**
|
|
8
|
+
* Set to `false` to disable logging.
|
|
9
|
+
*
|
|
10
|
+
* @category Internal
|
|
11
|
+
*/
|
|
12
|
+
export declare let shouldLogAuth: boolean;
|