auth-vir 5.0.2 → 5.0.3

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.
Files changed (48) hide show
  1. package/dist/auth-client/backend-auth.client.d.ts +263 -0
  2. package/dist/auth-client/backend-auth.client.js +396 -0
  3. package/dist/auth-client/frontend-auth.client.d.ts +113 -0
  4. package/dist/auth-client/frontend-auth.client.js +131 -0
  5. package/dist/auth-client/is-session-refresh-ready.d.ts +23 -0
  6. package/dist/auth-client/is-session-refresh-ready.js +21 -0
  7. package/dist/auth.d.ts +81 -0
  8. package/dist/auth.js +132 -0
  9. package/dist/cookie.d.ts +111 -0
  10. package/dist/cookie.js +137 -0
  11. package/dist/csrf-token.d.ts +33 -0
  12. package/dist/csrf-token.js +42 -0
  13. package/dist/generated/browser.d.ts +9 -0
  14. package/dist/generated/browser.js +17 -0
  15. package/dist/generated/client.d.ts +26 -0
  16. package/dist/generated/client.js +32 -0
  17. package/dist/generated/commonInputTypes.d.ts +122 -0
  18. package/dist/generated/commonInputTypes.js +1 -0
  19. package/dist/generated/enums.d.ts +1 -0
  20. package/dist/generated/enums.js +10 -0
  21. package/dist/generated/internal/class.d.ts +126 -0
  22. package/dist/generated/internal/class.js +85 -0
  23. package/dist/generated/internal/prismaNamespace.d.ts +545 -0
  24. package/dist/generated/internal/prismaNamespace.js +102 -0
  25. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +75 -0
  26. package/dist/generated/internal/prismaNamespaceBrowser.js +70 -0
  27. package/dist/generated/models/User.d.ts +980 -0
  28. package/dist/generated/models/User.js +1 -0
  29. package/dist/generated/models.d.ts +2 -0
  30. package/dist/generated/models.js +1 -0
  31. package/dist/generated/shapes.gen.d.ts +8 -0
  32. package/dist/generated/shapes.gen.js +11 -0
  33. package/dist/hash.d.ts +42 -0
  34. package/dist/hash.js +52 -0
  35. package/dist/headers.d.ts +19 -0
  36. package/dist/headers.js +32 -0
  37. package/dist/index.d.ts +11 -0
  38. package/dist/index.js +11 -0
  39. package/dist/jwt/jwt-keys.d.ts +44 -0
  40. package/dist/jwt/jwt-keys.js +57 -0
  41. package/dist/jwt/jwt-keys.script.d.ts +1 -0
  42. package/dist/jwt/jwt-keys.script.js +3 -0
  43. package/dist/jwt/jwt.d.ts +126 -0
  44. package/dist/jwt/jwt.js +109 -0
  45. package/dist/jwt/user-jwt.d.ts +44 -0
  46. package/dist/jwt/user-jwt.js +53 -0
  47. package/package.json +4 -4
  48. 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,396 @@
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
+ : undefined;
317
+ const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth);
318
+ const cookieParams = await this.getCookieParams({
319
+ isSignUpCookie,
320
+ requestHeaders,
321
+ });
322
+ const newCookieHeaders = existingUserIdResult
323
+ ? await this.refreshLoginHeaders({
324
+ userId,
325
+ cookieParams,
326
+ existingUserIdResult,
327
+ })
328
+ : await generateSuccessfulLoginHeaders(userId, cookieParams);
329
+ return {
330
+ ...newCookieHeaders,
331
+ 'set-cookie': mergeHeaderValues(newCookieHeaders['set-cookie'], discardOppositeCookieHeaders?.['set-cookie']),
332
+ ...(isSignUpCookie
333
+ ? {
334
+ [AuthHeaderName.IsSignUpAuth]: 'true',
335
+ }
336
+ : {}),
337
+ };
338
+ }
339
+ /** Combines `.getInsecureUser()` and `.getSecureUser()` into one method. */
340
+ async getInsecureOrSecureUser(params) {
341
+ const secureUser = await this.getSecureUser(params);
342
+ if (secureUser) {
343
+ return {
344
+ secureUser,
345
+ };
346
+ }
347
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
348
+ const insecureUser = await this.getInsecureUser(params);
349
+ return insecureUser
350
+ ? {
351
+ insecureUser,
352
+ }
353
+ : {};
354
+ }
355
+ /**
356
+ * @deprecated This only half authenticates the user. It should only be used in circumstances
357
+ * where JavaScript cannot be used to attach the CSRF token header to the request (like when
358
+ * opening a PDF file). Use `.getSecureUser()` instead, whenever possible.
359
+ */
360
+ async getInsecureUser({ requestHeaders, allowUserAuthRefresh, }) {
361
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
362
+ const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookie.Auth);
363
+ if (!userIdResult) {
364
+ this.logForUser({
365
+ user: undefined,
366
+ userId: undefined,
367
+ assumedUserParams: undefined,
368
+ }, 'getInsecureUser: failed to extract user ID from cookie (invalid JWT or missing cookie)');
369
+ return undefined;
370
+ }
371
+ const user = await this.getDatabaseUser({
372
+ isSignUpCookie: false,
373
+ userId: userIdResult.userId,
374
+ assumingUser: undefined,
375
+ requestHeaders,
376
+ });
377
+ if (!user) {
378
+ this.logForUser({
379
+ user: undefined,
380
+ userId: userIdResult.userId,
381
+ assumedUserParams: undefined,
382
+ }, 'getInsecureUser: user not found in database');
383
+ return undefined;
384
+ }
385
+ const refreshHeaders = allowUserAuthRefresh &&
386
+ (await this.createCookieRefreshHeaders({
387
+ userIdResult,
388
+ requestHeaders,
389
+ }));
390
+ return {
391
+ user,
392
+ isAssumed: false,
393
+ responseHeaders: refreshHeaders || {},
394
+ };
395
+ }
396
+ }