auth-vir 5.0.2 → 5.0.4
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 +263 -0
- package/dist/auth-client/backend-auth.client.js +398 -0
- package/dist/auth-client/frontend-auth.client.d.ts +113 -0
- package/dist/auth-client/frontend-auth.client.js +131 -0
- package/dist/auth-client/is-session-refresh-ready.d.ts +23 -0
- package/dist/auth-client/is-session-refresh-ready.js +21 -0
- package/dist/auth.d.ts +81 -0
- package/dist/auth.js +132 -0
- package/dist/cookie.d.ts +111 -0
- package/dist/cookie.js +137 -0
- package/dist/csrf-token.d.ts +33 -0
- package/dist/csrf-token.js +42 -0
- package/dist/generated/browser.d.ts +9 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.d.ts +26 -0
- package/dist/generated/client.js +32 -0
- package/dist/generated/commonInputTypes.d.ts +122 -0
- package/dist/generated/commonInputTypes.js +1 -0
- package/dist/generated/enums.d.ts +1 -0
- package/dist/generated/enums.js +10 -0
- package/dist/generated/internal/class.d.ts +126 -0
- package/dist/generated/internal/class.js +85 -0
- package/dist/generated/internal/prismaNamespace.d.ts +545 -0
- package/dist/generated/internal/prismaNamespace.js +102 -0
- package/dist/generated/internal/prismaNamespaceBrowser.d.ts +75 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +70 -0
- package/dist/generated/models/User.d.ts +980 -0
- package/dist/generated/models/User.js +1 -0
- package/dist/generated/models.d.ts +2 -0
- package/dist/generated/models.js +1 -0
- package/dist/generated/shapes.gen.d.ts +8 -0
- package/dist/generated/shapes.gen.js +11 -0
- package/dist/hash.d.ts +42 -0
- package/dist/hash.js +52 -0
- package/dist/headers.d.ts +19 -0
- package/dist/headers.js +32 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/jwt/jwt-keys.d.ts +44 -0
- package/dist/jwt/jwt-keys.js +57 -0
- package/dist/jwt/jwt-keys.script.d.ts +1 -0
- package/dist/jwt/jwt-keys.script.js +3 -0
- package/dist/jwt/jwt.d.ts +126 -0
- package/dist/jwt/jwt.js +109 -0
- package/dist/jwt/user-jwt.d.ts +44 -0
- package/dist/jwt/user-jwt.js +53 -0
- package/package.json +4 -4
- package/src/auth-client/backend-auth.client.ts +3 -0
- package/src/auth.ts +5 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { type AnyObject, type JsonCompatibleObject, type MaybePromise, type PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
+
import { type AnyDuration } from 'date-vir';
|
|
3
|
+
import { type IncomingHttpHeaders, type OutgoingHttpHeaders } from 'node:http';
|
|
4
|
+
import { type EmptyObject, type RequireExactlyOne, type RequireOneOrNone } from 'type-fest';
|
|
5
|
+
import { type UserIdResult } from '../auth.js';
|
|
6
|
+
import { type CookieParams } from '../cookie.js';
|
|
7
|
+
import { type CsrfHeaderNameOption } from '../csrf-token.js';
|
|
8
|
+
import { type JwtKeys, type RawJwtKeys } from '../jwt/jwt-keys.js';
|
|
9
|
+
import { type CreateJwtParams, type ParseJwtParams } from '../jwt/jwt.js';
|
|
10
|
+
/**
|
|
11
|
+
* Output from `BackendAuthClient.getSecureUser()`.
|
|
12
|
+
*
|
|
13
|
+
* @category Internal
|
|
14
|
+
*/
|
|
15
|
+
export type GetUserResult<DatabaseUser extends AnyObject> = {
|
|
16
|
+
/** The retrieved user. */
|
|
17
|
+
user: DatabaseUser;
|
|
18
|
+
/**
|
|
19
|
+
* When `true`, indicates that the current `user` result is as assumed user. This can only be
|
|
20
|
+
* `true` if you've configured user assuming in `BackendAuthClient`.
|
|
21
|
+
*/
|
|
22
|
+
isAssumed: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* This should be merged into your own response headers. It usually contains auth cookie
|
|
25
|
+
* duration refresh headers.
|
|
26
|
+
*/
|
|
27
|
+
responseHeaders: OutgoingHttpHeaders;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Config for {@link BackendAuthClient}.
|
|
31
|
+
*
|
|
32
|
+
* @category Internal
|
|
33
|
+
*/
|
|
34
|
+
export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId extends string | number, AssumedUserParams extends JsonCompatibleObject = EmptyObject> = Readonly<{
|
|
35
|
+
csrf: Readonly<CsrfHeaderNameOption>;
|
|
36
|
+
/** The origin of your backend that is offering auth cookies. */
|
|
37
|
+
serviceOrigin: string;
|
|
38
|
+
/** Finds the relevant user from your own database. */
|
|
39
|
+
getUserFromDatabase: (userParams: {
|
|
40
|
+
/** The user id extracted from the request cookie. */
|
|
41
|
+
userId: UserId;
|
|
42
|
+
/** Indicates that we're loading the user from a sign up cookie. */
|
|
43
|
+
isSignUpCookie: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* If this is set, we're attempting to load a database user for the purpose of assuming
|
|
46
|
+
* their user identity. Otherwise, this is `undefined`.
|
|
47
|
+
*/
|
|
48
|
+
assumingUser: AssumedUserParams | undefined;
|
|
49
|
+
requestHeaders: Readonly<IncomingHttpHeaders>;
|
|
50
|
+
}) => MaybePromise<DatabaseUser | undefined | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Get JWT keys produced by {@link generateNewJwtKeys}. Make sure that each time this is
|
|
53
|
+
* called, the same JWT keys are returned (do not call {@link generateNewJwtKeys} each time
|
|
54
|
+
* this is called). Any time the JWT keys change, all current sessions will terminate.
|
|
55
|
+
*/
|
|
56
|
+
getJwtKeys: () => MaybePromise<Readonly<RawJwtKeys>>;
|
|
57
|
+
/**
|
|
58
|
+
* When `isDev` is set, cookies do not require HTTPS (so they can be used with
|
|
59
|
+
* http://localhost).
|
|
60
|
+
*/
|
|
61
|
+
isDev: boolean;
|
|
62
|
+
} & PartialWithUndefined<{
|
|
63
|
+
/** If this returns true, logging will be enabled while handling the relevant session. */
|
|
64
|
+
enableLogging(params: {
|
|
65
|
+
user: DatabaseUser | undefined;
|
|
66
|
+
userId: UserId | undefined;
|
|
67
|
+
assumedUserParams: AssumedUserParams | undefined;
|
|
68
|
+
}): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Overwrite the header name used for tracking is an admin is assuming the identity of
|
|
71
|
+
* another user.
|
|
72
|
+
*/
|
|
73
|
+
assumedUserHeaderName: string;
|
|
74
|
+
/**
|
|
75
|
+
* Optionally generate a service origin from request headers. The generated origin is used
|
|
76
|
+
* for set-cookie headers.
|
|
77
|
+
*/
|
|
78
|
+
generateServiceOrigin(params: {
|
|
79
|
+
requestHeaders: Readonly<IncomingHttpHeaders>;
|
|
80
|
+
}): MaybePromise<undefined | string>;
|
|
81
|
+
/** If provided, logs will be sent to this method. */
|
|
82
|
+
log?: (message: string, extraData: AnyObject) => void;
|
|
83
|
+
/**
|
|
84
|
+
* Set this to allow specific users (determined by `canAssumeUser`) to assume the identity
|
|
85
|
+
* of other users. This should only be used for admins so that they can troubleshoot user
|
|
86
|
+
* issues.
|
|
87
|
+
*
|
|
88
|
+
* @see {@link AuthHeaderName}
|
|
89
|
+
*/
|
|
90
|
+
assumeUser: {
|
|
91
|
+
/**
|
|
92
|
+
* Parse the assumed user header value.
|
|
93
|
+
*
|
|
94
|
+
* @see {@link AuthHeaderName}
|
|
95
|
+
*/
|
|
96
|
+
parseAssumedUserHeaderValue: (
|
|
97
|
+
/**
|
|
98
|
+
* The assumed user header value.
|
|
99
|
+
*
|
|
100
|
+
* @see {@link AuthHeaderName}
|
|
101
|
+
*/
|
|
102
|
+
data: string) => MaybePromise<{
|
|
103
|
+
assumedUserParams: AssumedUserParams;
|
|
104
|
+
userId: UserId;
|
|
105
|
+
} | undefined>;
|
|
106
|
+
/**
|
|
107
|
+
* Return `true` to allow the current/original user to assume identities of other users.
|
|
108
|
+
* Return `false` to block it. It is recommended to only return `true` for admin users.
|
|
109
|
+
*
|
|
110
|
+
* @see {@link AuthHeaderName}
|
|
111
|
+
*/
|
|
112
|
+
canAssumeUser: (originalUser: DatabaseUser) => MaybePromise<boolean>;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* This determines how long a cookie will be valid until it needs to be refreshed.
|
|
116
|
+
*
|
|
117
|
+
* @default {minutes: 20}
|
|
118
|
+
*/
|
|
119
|
+
userSessionIdleTimeout: Readonly<AnyDuration>;
|
|
120
|
+
/**
|
|
121
|
+
* How long into a user's session when we should start trying to refresh their session.
|
|
122
|
+
*
|
|
123
|
+
* @default {minutes: 2}
|
|
124
|
+
*/
|
|
125
|
+
sessionRefreshStartTime: Readonly<AnyDuration>;
|
|
126
|
+
/**
|
|
127
|
+
* The maximum duration a session can last, regardless of activity. After this time, the
|
|
128
|
+
* user will be logged out even if they are actively using the application.
|
|
129
|
+
*
|
|
130
|
+
* @default {days: 1.5}
|
|
131
|
+
*/
|
|
132
|
+
maxSessionDuration: Readonly<AnyDuration>;
|
|
133
|
+
/**
|
|
134
|
+
* Allowed clock skew tolerance for JWT and CSRF token expiration checks. Accounts for
|
|
135
|
+
* differences between server and client clocks.
|
|
136
|
+
*
|
|
137
|
+
* @default {minutes: 5}
|
|
138
|
+
*/
|
|
139
|
+
allowedClockSkew: Readonly<AnyDuration>;
|
|
140
|
+
}>>;
|
|
141
|
+
/**
|
|
142
|
+
* An auth client for creating and validating JWTs embedded in cookies. This should only be used in
|
|
143
|
+
* a backend environment as it accesses native Node packages.
|
|
144
|
+
*
|
|
145
|
+
* @category Auth : Host
|
|
146
|
+
* @category Clients
|
|
147
|
+
*/
|
|
148
|
+
export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId extends string | number, AssumedUserParams extends AnyObject = EmptyObject> {
|
|
149
|
+
protected readonly config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams>;
|
|
150
|
+
protected cachedParsedJwtKeys: Record<string, Readonly<JwtKeys>>;
|
|
151
|
+
constructor(config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams>);
|
|
152
|
+
/** Conditionally logs a message if logging is enabled for the given user context. */
|
|
153
|
+
protected logForUser(params: {
|
|
154
|
+
user: DatabaseUser | undefined;
|
|
155
|
+
userId: UserId | undefined;
|
|
156
|
+
assumedUserParams: AssumedUserParams | undefined;
|
|
157
|
+
}, message: string, extra?: Record<string, unknown>): void;
|
|
158
|
+
/** Get all the parameters used for cookie generation. */
|
|
159
|
+
protected getCookieParams({ isSignUpCookie, requestHeaders, }: {
|
|
160
|
+
/**
|
|
161
|
+
* Set this to `true` when we are setting the initial cookie right after a user signs up.
|
|
162
|
+
* This allows them to auto-authorize when they verify their email address.
|
|
163
|
+
*
|
|
164
|
+
* This should only be set to `true` when a new user is signing up.
|
|
165
|
+
*/
|
|
166
|
+
isSignUpCookie: boolean;
|
|
167
|
+
requestHeaders: Readonly<IncomingHttpHeaders> | undefined;
|
|
168
|
+
}): Promise<Readonly<CookieParams>>;
|
|
169
|
+
/** Calls the provided `getUserFromDatabase` config. */
|
|
170
|
+
protected getDatabaseUser({ isSignUpCookie, userId, assumingUser, requestHeaders, }: {
|
|
171
|
+
userId: UserId | undefined;
|
|
172
|
+
assumingUser: AssumedUserParams | undefined;
|
|
173
|
+
isSignUpCookie: boolean;
|
|
174
|
+
requestHeaders: IncomingHttpHeaders;
|
|
175
|
+
}): Promise<undefined | DatabaseUser>;
|
|
176
|
+
/** Creates a `'cookie-set'` header to refresh the user's session cookie. */
|
|
177
|
+
protected createCookieRefreshHeaders({ userIdResult, requestHeaders, }: {
|
|
178
|
+
userIdResult: Readonly<UserIdResult<UserId>>;
|
|
179
|
+
requestHeaders: IncomingHttpHeaders;
|
|
180
|
+
}): Promise<OutgoingHttpHeaders | undefined>;
|
|
181
|
+
/** Reads the user's assumed user headers and, if configured, gets the assumed user. */
|
|
182
|
+
protected getAssumedUser({ requestHeaders, user, }: {
|
|
183
|
+
user: DatabaseUser;
|
|
184
|
+
requestHeaders: IncomingHttpHeaders;
|
|
185
|
+
}): Promise<DatabaseUser | undefined>;
|
|
186
|
+
/** Securely extract a user from their request headers. */
|
|
187
|
+
getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }: {
|
|
188
|
+
requestHeaders: IncomingHttpHeaders;
|
|
189
|
+
isSignUpCookie: boolean;
|
|
190
|
+
/**
|
|
191
|
+
* If true, this method will generate headers to refresh the user's auth session. This
|
|
192
|
+
* should likely only be done with a specific endpoint, like whatever endpoint you trigger
|
|
193
|
+
* with the frontend auth client's `checkUser.performCheck` callback.
|
|
194
|
+
*/
|
|
195
|
+
allowUserAuthRefresh: boolean;
|
|
196
|
+
}): Promise<GetUserResult<DatabaseUser> | undefined>;
|
|
197
|
+
/**
|
|
198
|
+
* Get all the JWT params used when creating the auth cookie, in case you need them for
|
|
199
|
+
* something else too.
|
|
200
|
+
*/
|
|
201
|
+
getJwtParams(): Promise<Readonly<CreateJwtParams> & ParseJwtParams>;
|
|
202
|
+
/** Use these headers to log out the user. */
|
|
203
|
+
createLogoutHeaders(params: Readonly<RequireExactlyOne<{
|
|
204
|
+
allCookies: true;
|
|
205
|
+
isSignUpCookie: boolean;
|
|
206
|
+
}> & {
|
|
207
|
+
/** Overrides the client's already established `serviceOrigin`. */
|
|
208
|
+
serviceOrigin?: string | undefined;
|
|
209
|
+
}>): Promise<Record<string, string | string[]> & {
|
|
210
|
+
'set-cookie': string[];
|
|
211
|
+
}>;
|
|
212
|
+
/**
|
|
213
|
+
* Refreshes a login session by reissuing the auth cookie with the same CSRF token instead of
|
|
214
|
+
* generating a new one.
|
|
215
|
+
*/
|
|
216
|
+
protected refreshLoginHeaders({ userId, cookieParams, existingUserIdResult, }: {
|
|
217
|
+
userId: UserId;
|
|
218
|
+
cookieParams: Readonly<CookieParams>;
|
|
219
|
+
existingUserIdResult: Readonly<UserIdResult<UserId>>;
|
|
220
|
+
}): Promise<Record<string, string | string[]>>;
|
|
221
|
+
/** Use these headers to log a user in. */
|
|
222
|
+
createLoginHeaders({ userId, requestHeaders, isSignUpCookie, }: {
|
|
223
|
+
userId: UserId;
|
|
224
|
+
requestHeaders: IncomingHttpHeaders;
|
|
225
|
+
isSignUpCookie: boolean;
|
|
226
|
+
}): Promise<OutgoingHttpHeaders>;
|
|
227
|
+
/** Combines `.getInsecureUser()` and `.getSecureUser()` into one method. */
|
|
228
|
+
getInsecureOrSecureUser(params: {
|
|
229
|
+
requestHeaders: IncomingHttpHeaders;
|
|
230
|
+
isSignUpCookie: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* If true, this method will generate headers to refresh the user's auth session. This
|
|
233
|
+
* should likely only be done with a specific endpoint, like whatever endpoint you trigger
|
|
234
|
+
* with the frontend auth client's `checkUser.performCheck` callback.
|
|
235
|
+
*/
|
|
236
|
+
allowUserAuthRefresh: boolean;
|
|
237
|
+
/** Overrides the client's already established `serviceOrigin`. */
|
|
238
|
+
serviceOrigin?: string | undefined;
|
|
239
|
+
}): Promise<RequireOneOrNone<{
|
|
240
|
+
secureUser: GetUserResult<DatabaseUser>;
|
|
241
|
+
/**
|
|
242
|
+
* @deprecated This only half authenticates the user. It should only be used in
|
|
243
|
+
* circumstances where JavaScript cannot be used to attach the CSRF token header to
|
|
244
|
+
* the request (like when opening a PDF file). Use `.getSecureUser()` instead,
|
|
245
|
+
* whenever possible.
|
|
246
|
+
*/
|
|
247
|
+
insecureUser: GetUserResult<DatabaseUser>;
|
|
248
|
+
}>>;
|
|
249
|
+
/**
|
|
250
|
+
* @deprecated This only half authenticates the user. It should only be used in circumstances
|
|
251
|
+
* where JavaScript cannot be used to attach the CSRF token header to the request (like when
|
|
252
|
+
* opening a PDF file). Use `.getSecureUser()` instead, whenever possible.
|
|
253
|
+
*/
|
|
254
|
+
getInsecureUser({ requestHeaders, allowUserAuthRefresh, }: {
|
|
255
|
+
requestHeaders: IncomingHttpHeaders;
|
|
256
|
+
/**
|
|
257
|
+
* If true, this method will generate headers to refresh the user's auth session. This
|
|
258
|
+
* should likely only be done with a specific endpoint, like whatever endpoint you trigger
|
|
259
|
+
* with the frontend auth client's `checkUser.performCheck` callback.
|
|
260
|
+
*/
|
|
261
|
+
allowUserAuthRefresh: boolean;
|
|
262
|
+
}): Promise<GetUserResult<DatabaseUser> | undefined>;
|
|
263
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
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 clearingAllCookies = !!params.allCookies;
|
|
269
|
+
const signUpCookieHeaders = params.allCookies || params.isSignUpCookie
|
|
270
|
+
? generateLogoutHeaders(await this.getCookieParams({
|
|
271
|
+
isSignUpCookie: true,
|
|
272
|
+
requestHeaders: undefined,
|
|
273
|
+
}), {
|
|
274
|
+
preserveCsrf: !clearingAllCookies,
|
|
275
|
+
})
|
|
276
|
+
: undefined;
|
|
277
|
+
const authCookieHeaders = params.allCookies || !params.isSignUpCookie
|
|
278
|
+
? generateLogoutHeaders(await this.getCookieParams({
|
|
279
|
+
isSignUpCookie: false,
|
|
280
|
+
requestHeaders: undefined,
|
|
281
|
+
}), {
|
|
282
|
+
preserveCsrf: !clearingAllCookies,
|
|
283
|
+
})
|
|
284
|
+
: undefined;
|
|
285
|
+
return {
|
|
286
|
+
'set-cookie': mergeHeaderValues(signUpCookieHeaders?.['set-cookie'], authCookieHeaders?.['set-cookie']),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Refreshes a login session by reissuing the auth cookie with the same CSRF token instead of
|
|
291
|
+
* generating a new one.
|
|
292
|
+
*/
|
|
293
|
+
async refreshLoginHeaders({ userId, cookieParams, existingUserIdResult, }) {
|
|
294
|
+
const authCookie = await generateAuthCookie({
|
|
295
|
+
csrfToken: existingUserIdResult.csrfToken,
|
|
296
|
+
userId,
|
|
297
|
+
sessionStartedAt: existingUserIdResult.sessionStartedAt,
|
|
298
|
+
}, cookieParams);
|
|
299
|
+
const csrfCookie = generateCsrfCookie(existingUserIdResult.csrfToken, cookieParams);
|
|
300
|
+
return {
|
|
301
|
+
'set-cookie': [
|
|
302
|
+
authCookie,
|
|
303
|
+
csrfCookie,
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/** Use these headers to log a user in. */
|
|
308
|
+
async createLoginHeaders({ userId, requestHeaders, isSignUpCookie, }) {
|
|
309
|
+
const oppositeCookieName = isSignUpCookie ? AuthCookie.Auth : AuthCookie.SignUp;
|
|
310
|
+
const hasExistingOppositeCookie = requestHeaders.cookie?.includes(`${oppositeCookieName}=`);
|
|
311
|
+
const discardOppositeCookieHeaders = hasExistingOppositeCookie
|
|
312
|
+
? generateLogoutHeaders(await this.getCookieParams({
|
|
313
|
+
isSignUpCookie: !isSignUpCookie,
|
|
314
|
+
requestHeaders,
|
|
315
|
+
}), {
|
|
316
|
+
preserveCsrf: true,
|
|
317
|
+
})
|
|
318
|
+
: undefined;
|
|
319
|
+
const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth);
|
|
320
|
+
const cookieParams = await this.getCookieParams({
|
|
321
|
+
isSignUpCookie,
|
|
322
|
+
requestHeaders,
|
|
323
|
+
});
|
|
324
|
+
const newCookieHeaders = existingUserIdResult
|
|
325
|
+
? await this.refreshLoginHeaders({
|
|
326
|
+
userId,
|
|
327
|
+
cookieParams,
|
|
328
|
+
existingUserIdResult,
|
|
329
|
+
})
|
|
330
|
+
: await generateSuccessfulLoginHeaders(userId, cookieParams);
|
|
331
|
+
return {
|
|
332
|
+
...newCookieHeaders,
|
|
333
|
+
'set-cookie': mergeHeaderValues(newCookieHeaders['set-cookie'], discardOppositeCookieHeaders?.['set-cookie']),
|
|
334
|
+
...(isSignUpCookie
|
|
335
|
+
? {
|
|
336
|
+
[AuthHeaderName.IsSignUpAuth]: 'true',
|
|
337
|
+
}
|
|
338
|
+
: {}),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/** Combines `.getInsecureUser()` and `.getSecureUser()` into one method. */
|
|
342
|
+
async getInsecureOrSecureUser(params) {
|
|
343
|
+
const secureUser = await this.getSecureUser(params);
|
|
344
|
+
if (secureUser) {
|
|
345
|
+
return {
|
|
346
|
+
secureUser,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
350
|
+
const insecureUser = await this.getInsecureUser(params);
|
|
351
|
+
return insecureUser
|
|
352
|
+
? {
|
|
353
|
+
insecureUser,
|
|
354
|
+
}
|
|
355
|
+
: {};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* @deprecated This only half authenticates the user. It should only be used in circumstances
|
|
359
|
+
* where JavaScript cannot be used to attach the CSRF token header to the request (like when
|
|
360
|
+
* opening a PDF file). Use `.getSecureUser()` instead, whenever possible.
|
|
361
|
+
*/
|
|
362
|
+
async getInsecureUser({ requestHeaders, allowUserAuthRefresh, }) {
|
|
363
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
364
|
+
const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookie.Auth);
|
|
365
|
+
if (!userIdResult) {
|
|
366
|
+
this.logForUser({
|
|
367
|
+
user: undefined,
|
|
368
|
+
userId: undefined,
|
|
369
|
+
assumedUserParams: undefined,
|
|
370
|
+
}, 'getInsecureUser: failed to extract user ID from cookie (invalid JWT or missing cookie)');
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
const user = await this.getDatabaseUser({
|
|
374
|
+
isSignUpCookie: false,
|
|
375
|
+
userId: userIdResult.userId,
|
|
376
|
+
assumingUser: undefined,
|
|
377
|
+
requestHeaders,
|
|
378
|
+
});
|
|
379
|
+
if (!user) {
|
|
380
|
+
this.logForUser({
|
|
381
|
+
user: undefined,
|
|
382
|
+
userId: userIdResult.userId,
|
|
383
|
+
assumedUserParams: undefined,
|
|
384
|
+
}, 'getInsecureUser: user not found in database');
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
const refreshHeaders = allowUserAuthRefresh &&
|
|
388
|
+
(await this.createCookieRefreshHeaders({
|
|
389
|
+
userIdResult,
|
|
390
|
+
requestHeaders,
|
|
391
|
+
}));
|
|
392
|
+
return {
|
|
393
|
+
user,
|
|
394
|
+
isAssumed: false,
|
|
395
|
+
responseHeaders: refreshHeaders || {},
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
}
|