auth-vir 1.3.1 → 2.0.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.
Files changed (65) hide show
  1. package/README.md +27 -18
  2. package/dist/auth-client/backend-auth.client.d.ts +177 -0
  3. package/dist/auth-client/backend-auth.client.js +232 -0
  4. package/dist/auth-client/frontend-auth.client.d.ts +64 -0
  5. package/dist/auth-client/frontend-auth.client.js +107 -0
  6. package/dist/auth.d.ts +33 -47
  7. package/dist/auth.js +36 -36
  8. package/dist/cookie.d.ts +15 -4
  9. package/dist/cookie.js +17 -5
  10. package/dist/csrf-token.d.ts +113 -3
  11. package/dist/csrf-token.js +101 -5
  12. package/dist/generated/browser.d.ts +9 -0
  13. package/dist/generated/browser.js +16 -0
  14. package/dist/generated/client.d.ts +26 -0
  15. package/dist/generated/client.js +31 -0
  16. package/dist/generated/commonInputTypes.d.ts +122 -0
  17. package/dist/generated/enums.d.ts +1 -0
  18. package/dist/generated/enums.js +9 -0
  19. package/dist/generated/internal/class.d.ts +126 -0
  20. package/dist/generated/internal/class.js +84 -0
  21. package/dist/generated/internal/prismaNamespace.d.ts +544 -0
  22. package/dist/generated/internal/prismaNamespace.js +101 -0
  23. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +75 -0
  24. package/dist/generated/internal/prismaNamespaceBrowser.js +69 -0
  25. package/dist/generated/models/User.d.ts +983 -0
  26. package/dist/generated/models/User.js +1 -0
  27. package/dist/generated/models.d.ts +2 -0
  28. package/dist/generated/models.js +1 -0
  29. package/dist/generated/shapes.gen.d.ts +8 -0
  30. package/dist/generated/shapes.gen.js +11 -0
  31. package/dist/headers.d.ts +20 -0
  32. package/dist/headers.js +33 -0
  33. package/dist/index.d.ts +5 -3
  34. package/dist/index.js +5 -3
  35. package/dist/jwt/jwt-keys.script.d.ts +1 -0
  36. package/dist/{jwt.d.ts → jwt/jwt.d.ts} +11 -2
  37. package/dist/{jwt.js → jwt/jwt.js} +14 -3
  38. package/dist/{user-jwt.d.ts → jwt/user-jwt.d.ts} +8 -8
  39. package/dist/{user-jwt.js → jwt/user-jwt.js} +12 -9
  40. package/package.json +17 -12
  41. package/src/auth-client/backend-auth.client.ts +500 -0
  42. package/src/auth-client/frontend-auth.client.ts +182 -0
  43. package/src/auth.ts +100 -78
  44. package/src/cookie.ts +20 -8
  45. package/src/csrf-token.ts +196 -5
  46. package/src/generated/browser.ts +23 -0
  47. package/src/generated/client.ts +47 -0
  48. package/src/generated/commonInputTypes.ts +147 -0
  49. package/src/generated/enums.ts +14 -0
  50. package/src/generated/internal/class.ts +236 -0
  51. package/src/generated/internal/prismaNamespace.ts +761 -0
  52. package/src/generated/internal/prismaNamespaceBrowser.ts +102 -0
  53. package/src/generated/models/User.ts +1135 -0
  54. package/src/generated/models.ts +11 -0
  55. package/src/generated/shapes.gen.ts +15 -0
  56. package/src/headers.ts +35 -0
  57. package/src/index.ts +5 -3
  58. package/src/{jwt.ts → jwt/jwt.ts} +34 -5
  59. package/src/{user-jwt.ts → jwt/user-jwt.ts} +22 -13
  60. /package/dist/{jwt-keys.script.d.ts → generated/commonInputTypes.js} +0 -0
  61. /package/dist/{jwt-keys.d.ts → jwt/jwt-keys.d.ts} +0 -0
  62. /package/dist/{jwt-keys.js → jwt/jwt-keys.js} +0 -0
  63. /package/dist/{jwt-keys.script.js → jwt/jwt-keys.script.js} +0 -0
  64. /package/src/{jwt-keys.script.ts → jwt/jwt-keys.script.ts} +0 -0
  65. /package/src/{jwt-keys.ts → jwt/jwt-keys.ts} +0 -0
package/README.md CHANGED
@@ -13,6 +13,10 @@ npm i auth-vir
13
13
 
14
14
  # Usage
15
15
 
16
+ ## Easy usage
17
+
18
+ For the easiest usage, construct and use `BackendAuthClient` on your server and `FrontendAuthClient` in your frontend.
19
+
16
20
  ## Password hashing
17
21
 
18
22
  - Hash a user created password:
@@ -49,9 +53,9 @@ npm i auth-vir
49
53
 
50
54
  Use this on your host / server / backend to authenticate client / frontend requests.
51
55
 
52
- 1. Expose the [`csrfTokenHeaderName`](https://electrovir.github.io/auth-vir/variables/csrfTokenHeaderName.html) (or just `'csrf-token'`) header via CORS headers with either of the following options:
53
- 1. Set `customHeaders: [csrfTokenHeaderName]` in `implementService` from [`@rest-vir/implement-service`](https://www.npmjs.com/package/@rest-vir/implement-service).
54
- 2. Set the header `Access-Control-Allow-Headers` to (at least) `csrfTokenHeaderName`.
56
+ 1. Expose the [`AuthHeaderName.CsrfToken`](https://electrovir.github.io/auth-vir/variables/AuthHeaderName.html) (or just `'csrf-token'`) header via CORS headers with either of the following options:
57
+ 1. Set `customHeaders: [AuthHeaderName.CsrfToken]` in `implementService` from [`@rest-vir/implement-service`](https://www.npmjs.com/package/@rest-vir/implement-service).
58
+ 2. Set the header `Access-Control-Allow-Headers` to (at least) `AuthHeaderName.CsrfToken`.
55
59
  2. Set the `Access-Control-Allow-Origin` header (it cannot be `*`) and properly implement CORS headers and responses.
56
60
  3. Generate JWT signing and encryption keys with one of the following:
57
61
  - Run `npx auth-vir`: the generated keys will be printed to your console.
@@ -79,6 +83,8 @@ import {
79
83
  type CreateJwtParams,
80
84
  } from 'auth-vir';
81
85
 
86
+ type MyUserId = string;
87
+
82
88
  /**
83
89
  * Use this for a /login endpoint.
84
90
  *
@@ -125,7 +131,9 @@ export async function createUser(
125
131
  * This loads the current user from their auth cookie and CSRF token.
126
132
  */
127
133
  export async function getAuthenticatedUser(request: ClientRequest) {
128
- const userId = await extractUserIdFromRequestHeaders(request.getHeaders(), jwtParams);
134
+ const userId = (
135
+ await extractUserIdFromRequestHeaders<MyUserId>(request.getHeaders(), jwtParams)
136
+ )?.userId;
129
137
  const user = userId ? findUserInDatabaseById(userId) : undefined;
130
138
 
131
139
  if (!userId || !user) {
@@ -180,7 +188,12 @@ function findUserInDatabaseByUsername(username: string) {
180
188
  };
181
189
  }
182
190
 
183
- function findUserInDatabaseById(userId: string): undefined | {id: string; username: string} {
191
+ function findUserInDatabaseById(userId: MyUserId):
192
+ | undefined
193
+ | {
194
+ id: MyUserId;
195
+ username: string;
196
+ } {
184
197
  /** This should connect to your database and find a user matching the given user id. */
185
198
 
186
199
  return {
@@ -230,7 +243,7 @@ Use this on your client / frontend for storing and sending session authorization
230
243
 
231
244
  1. Send a login fetch request to your host / server / backend with `{credentials: 'include'}` set on the request.
232
245
  2. Pass the `Response` from step 1 into [`handleAuthResponse`](https://electrovir.github.io/auth-vir/functions/handleAuthResponse.html).
233
- 3. In all subsequent fetch requests to the host / server / backend, set `{credentials: 'include'}` and include `{headers: {[csrfTokenHeaderName]: getCurrentCsrfToken()}}`.
246
+ 3. In all subsequent fetch requests to the host / server / backend, set `{credentials: 'include'}` and include `{headers: {[AuthHeaderName.CsrfToken]: getCurrentCsrfToken()}}`.
234
247
  4. Upon user logout, call [`wipeCurrentCsrfToken()`](https://electrovir.github.io/auth-vir/functions/wipeCurrentCsrfToken.html)
235
248
 
236
249
  Here's a full example of how to use all the client / frontend side auth functionality:
@@ -239,19 +252,15 @@ Here's a full example of how to use all the client / frontend side auth function
239
252
 
240
253
  ```TypeScript
241
254
  import {HttpStatus} from '@augment-vir/common';
242
- import {
243
- csrfTokenHeaderName,
244
- getCurrentCsrfToken,
245
- handleAuthResponse,
246
- wipeCurrentCsrfToken,
247
- } from 'auth-vir';
255
+ import {AuthHeaderName} from '../headers.js';
256
+ import {getCurrentCsrfToken, handleAuthResponse, wipeCurrentCsrfToken} from 'auth-vir';
248
257
 
249
258
  /** Call this when the user logs in for the first time this session. */
250
259
  export async function sendLoginRequest(
251
260
  userLoginData: {username: string; password: string},
252
261
  loginUrl: string,
253
262
  ) {
254
- if (getCurrentCsrfToken()) {
263
+ if (getCurrentCsrfToken().csrfToken) {
255
264
  throw new Error('Already logged in.');
256
265
  }
257
266
 
@@ -272,7 +281,7 @@ export async function sendAuthenticatedRequest(
272
281
  requestInit: Omit<RequestInit, 'headers'> = {},
273
282
  headers: Record<string, string> = {},
274
283
  ) {
275
- const csrfToken = getCurrentCsrfToken();
284
+ const {csrfToken} = getCurrentCsrfToken();
276
285
 
277
286
  if (!csrfToken) {
278
287
  throw new Error('Not authenticated.');
@@ -283,7 +292,7 @@ export async function sendAuthenticatedRequest(
283
292
  credentials: 'include',
284
293
  headers: {
285
294
  ...headers,
286
- [csrfTokenHeaderName]: csrfToken,
295
+ [AuthHeaderName.CsrfToken]: csrfToken.token,
287
296
  },
288
297
  });
289
298
 
@@ -310,8 +319,8 @@ export function logout() {
310
319
 
311
320
  All of these configurations must be set for the auth exports in this package to function properly:
312
321
 
313
- - Expose the [`csrfTokenHeaderName`](https://electrovir.github.io/auth-vir/variables/csrfTokenHeaderName.html) (or just `'csrf-token'`) header via CORS headers with either of the following options:
314
- 1. Set `customHeaders: [csrfTokenHeaderName]` in `implementService` from [`@rest-vir/implement-service`](https://www.npmjs.com/package/@rest-vir/implement-service).
315
- 2. Set the header `Access-Control-Allow-Headers` to (at least) `csrfTokenHeaderName`.
322
+ - Expose the [`AuthHeaderName.CsrfToken`](https://electrovir.github.io/auth-vir/variables/AuthHeaderName.html) (or just `'csrf-token'`) header via CORS headers with either of the following options:
323
+ 1. Set `customHeaders: [AuthHeaderName.CsrfToken]` in `implementService` from [`@rest-vir/implement-service`](https://www.npmjs.com/package/@rest-vir/implement-service).
324
+ 2. Set the header `Access-Control-Allow-Headers` to (at least) `AuthHeaderName.CsrfToken`.
316
325
  - Set `credentials: include` in all fetch requests on the client that need to use or set the auth cookie.
317
326
  - Server CORS should set `Access-Control-Allow-Origin` (it cannot be `*`).
@@ -0,0 +1,177 @@
1
+ import { type AnyObject, type JsonCompatibleObject, type MaybePromise, type PartialWithUndefined, type RequiredAndNotNull } from '@augment-vir/common';
2
+ import { type AnyDuration } from 'date-vir';
3
+ import { type IncomingHttpHeaders, type OutgoingHttpHeaders } from 'node:http';
4
+ import { type EmptyObject } from 'type-fest';
5
+ import { type UserIdResult } from '../auth.js';
6
+ import { type CookieParams } from '../cookie.js';
7
+ import { AuthHeaderName } from '../headers.js';
8
+ import { type JwtKeys, type RawJwtKeys } from '../jwt/jwt-keys.js';
9
+ import { type CreateJwtParams } 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, CsrfHeaderName extends string = AuthHeaderName.CsrfToken, AssumedUserParams extends JsonCompatibleObject = EmptyObject> = Readonly<{
35
+ /** The origin of your backend that is offering auth cookies. */
36
+ serviceOrigin: string;
37
+ /** Finds the relevant user from your own database. */
38
+ getUserFromDatabase: (userParams: {
39
+ /** The user id extracted from the request cookie. */
40
+ userId: UserId;
41
+ /** Indicates that we're loading the user from a sign up cookie. */
42
+ isSignUpCookie: boolean;
43
+ /**
44
+ * If this is set, we're attempting to load a database user for the purpose of assuming
45
+ * their user identity. Otherwise, this is `undefined`.
46
+ */
47
+ assumedUserParams: AssumedUserParams | undefined;
48
+ }) => MaybePromise<DatabaseUser | undefined | null>;
49
+ /**
50
+ * Get JWT keys produced by {@link generateNewJwtKeys}. Make sure that each time this is
51
+ * called, the same JWT keys are returned (do not call {@link generateNewJwtKeys} each time
52
+ * this is called). Any time the JWT keys change, all current sessions will terminate.
53
+ */
54
+ getJetKeys: () => MaybePromise<Readonly<RawJwtKeys>>;
55
+ /**
56
+ * When `isDev` is set, cookies do not require HTTPS (so they can be used with
57
+ * http://localhost).
58
+ */
59
+ isDev: boolean;
60
+ } & PartialWithUndefined<{
61
+ /**
62
+ * Set this to allow specific users (determined by `canAssumeUser`) to assume the identity
63
+ * of other users. This should only be used for admins so that they can troubleshoot user
64
+ * issues.
65
+ *
66
+ * @see {@link AuthHeaderName}
67
+ */
68
+ assumeUser: {
69
+ /**
70
+ * Handles assumed user header value.
71
+ *
72
+ * @see {@link AuthHeaderName}
73
+ */
74
+ handleAssumedUserData: (
75
+ /**
76
+ * The assumed user header value.
77
+ *
78
+ * @see {@link AuthHeaderName}
79
+ */
80
+ data: string) => MaybePromise<{
81
+ assumedUserParams: AssumedUserParams;
82
+ userId: UserId;
83
+ } | undefined>;
84
+ /**
85
+ * Return `true` to allow the current user (by the given id) to assume identities of
86
+ * other users. Return `false` to block it. It is recommended to only return `true` for
87
+ * admin users.
88
+ *
89
+ * @see {@link AuthHeaderName}
90
+ */
91
+ canAssumeUser: (params: {
92
+ userId: UserId;
93
+ }) => MaybePromise<boolean>;
94
+ };
95
+ /**
96
+ * This determines how long a cookie will be valid until it needs to be refreshed.
97
+ *
98
+ * @default {minutes: 20}
99
+ */
100
+ userSessionIdleTimeout: Readonly<AnyDuration>;
101
+ /**
102
+ * How long before a user's session times out when we should start trying to refresh their
103
+ * session.
104
+ *
105
+ * @default {minutes: 5}
106
+ */
107
+ sessionRefreshThreshold: Readonly<AnyDuration>;
108
+ overrides: PartialWithUndefined<{
109
+ csrfHeaderName: CsrfHeaderName;
110
+ assumedUserHeaderName: string;
111
+ }>;
112
+ }>>;
113
+ /**
114
+ * An auth client for creating and validating JWTs embedded in cookies. This should only be used in
115
+ * a backend environment as it accesses native Node packages.
116
+ *
117
+ * @category Auth : Host
118
+ * @category Client
119
+ */
120
+ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId extends string | number, CsrfHeaderName extends string = AuthHeaderName.CsrfToken, AssumedUserParams extends AnyObject = EmptyObject> {
121
+ protected readonly config: BackendAuthClientConfig<DatabaseUser, UserId, CsrfHeaderName, AssumedUserParams>;
122
+ protected cachedParsedJwtKeys: Record<string, Readonly<JwtKeys>>;
123
+ constructor(config: BackendAuthClientConfig<DatabaseUser, UserId, CsrfHeaderName, AssumedUserParams>);
124
+ /** Get all the parameters used for cookie generation. */
125
+ protected getCookieParams({ isSignUpCookie, }: {
126
+ /**
127
+ * Set this to `true` when we are setting the initial cookie right after a user signs up.
128
+ * This allows them to auto-authorize when they verify their email address.
129
+ *
130
+ * This should only be set to `true` when a new user is signing up.
131
+ */
132
+ isSignUpCookie?: boolean | undefined;
133
+ }): Promise<Readonly<CookieParams>>;
134
+ /** Calls the provided `getUserFromDatabase` config. */
135
+ protected getDatabaseUser({ isSignUpCookie, userId, assumedUserParams, }: {
136
+ userId: UserId | undefined;
137
+ assumedUserParams: AssumedUserParams | undefined;
138
+ isSignUpCookie: boolean;
139
+ }): Promise<undefined | DatabaseUser>;
140
+ /** Creates a `'cookie-set'` header to refresh the user's session cookie. */
141
+ protected createCookieRefreshHeaders({ userIdResult, }: {
142
+ userIdResult: Readonly<UserIdResult<UserId>>;
143
+ }): Promise<OutgoingHttpHeaders | undefined>;
144
+ /** Reads the user's assumed user headers and, if configured, gets the assumed user. */
145
+ protected getAssumedUser({ headers, originalUserId, }: {
146
+ originalUserId: UserId | undefined;
147
+ headers: IncomingHttpHeaders;
148
+ }): Promise<DatabaseUser | undefined>;
149
+ /** Securely extract a user from their request headers. */
150
+ getSecureUser({ requestHeaders, isSignUpCookie, }: {
151
+ requestHeaders: IncomingHttpHeaders;
152
+ isSignUpCookie?: boolean | undefined;
153
+ }): Promise<GetUserResult<DatabaseUser> | undefined>;
154
+ /**
155
+ * Get all the JWT params used when creating the auth cookie, in case you need them for
156
+ * something else too.
157
+ */
158
+ getJwtParams(): Promise<Readonly<CreateJwtParams>>;
159
+ /** Use these headers to log out the user. */
160
+ createLogoutHeaders(): Promise<{
161
+ 'set-cookie': string;
162
+ } & Record<CsrfHeaderName, string>>;
163
+ /** Use these headers to log a user in. */
164
+ createLoginHeaders({ userId, requestHeaders, isSignUpCookie, }: {
165
+ userId: UserId;
166
+ requestHeaders: IncomingHttpHeaders;
167
+ isSignUpCookie: boolean;
168
+ }): Promise<Pick<RequiredAndNotNull<OutgoingHttpHeaders>, 'set-cookie'> & Record<CsrfHeaderName, string>>;
169
+ /**
170
+ * @deprecated This only half authenticates the user. It should only be used in circumstances
171
+ * where JavaScript cannot be used to attach the CSRF token header to the request (like when
172
+ * opening a PDF file). Use `.getSecureUser()` instead, whenever possible.
173
+ */
174
+ getInsecureUser({ headers, }: {
175
+ headers: IncomingHttpHeaders;
176
+ }): Promise<GetUserResult<DatabaseUser> | undefined>;
177
+ }
@@ -0,0 +1,232 @@
1
+ import { ensureArray, } from '@augment-vir/common';
2
+ import { calculateRelativeDate, getNowInUtcTimezone, isDateAfter } from 'date-vir';
3
+ import { extractUserIdFromRequestHeaders, generateLogoutHeaders, generateSuccessfulLoginHeaders, insecureExtractUserIdFromCookieAlone, } from '../auth.js';
4
+ import { AuthCookieName } from '../cookie.js';
5
+ import { AuthHeaderName, mergeHeaderValues } from '../headers.js';
6
+ import { parseJwtKeys } from '../jwt/jwt-keys.js';
7
+ const defaultSessionIdleTimeout = {
8
+ minutes: 20,
9
+ };
10
+ /**
11
+ * An auth client for creating and validating JWTs embedded in cookies. This should only be used in
12
+ * a backend environment as it accesses native Node packages.
13
+ *
14
+ * @category Auth : Host
15
+ * @category Client
16
+ */
17
+ export class BackendAuthClient {
18
+ config;
19
+ cachedParsedJwtKeys = {};
20
+ constructor(config) {
21
+ this.config = config;
22
+ }
23
+ /** Get all the parameters used for cookie generation. */
24
+ async getCookieParams({ isSignUpCookie, }) {
25
+ return {
26
+ cookieDuration: this.config.userSessionIdleTimeout || defaultSessionIdleTimeout,
27
+ hostOrigin: this.config.serviceOrigin,
28
+ jwtParams: await this.getJwtParams(),
29
+ isDev: this.config.isDev,
30
+ cookieName: isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth,
31
+ };
32
+ }
33
+ /** Calls the provided `getUserFromDatabase` config. */
34
+ async getDatabaseUser({ isSignUpCookie, userId, assumedUserParams, }) {
35
+ if (!userId) {
36
+ return undefined;
37
+ }
38
+ const authenticatedUser = await this.config.getUserFromDatabase({
39
+ assumedUserParams,
40
+ userId,
41
+ isSignUpCookie,
42
+ });
43
+ if (!authenticatedUser) {
44
+ return undefined;
45
+ }
46
+ return authenticatedUser;
47
+ }
48
+ /** Creates a `'cookie-set'` header to refresh the user's session cookie. */
49
+ async createCookieRefreshHeaders({ userIdResult, }) {
50
+ const now = getNowInUtcTimezone();
51
+ /** Double check that the JWT hasn't already expired. */
52
+ const isExpiredAlready = isDateAfter({
53
+ fullDate: now,
54
+ relativeTo: userIdResult.jwtExpiration,
55
+ });
56
+ if (isExpiredAlready) {
57
+ return undefined;
58
+ }
59
+ /**
60
+ * This check performs the following: the current time + the refresh threshold > JWT
61
+ * expiration.
62
+ *
63
+ * Visually, this check looks like this:
64
+ *
65
+ * X C=======Y=======R Z
66
+ *
67
+ * - C = current time
68
+ * - R = C + refresh threshold
69
+ * - `=` = the time frame in which {@link isRefreshReady} = true.
70
+ * - X = JWT expiration that has already expired (rejected by {@link isExpiredAlready}.
71
+ * - Y = JWT expiration within the refresh threshold: {@link isRefreshReady} = true.
72
+ * - Z = JWT expiration outside the refresh threshold: {@link isRefreshReady} = false.
73
+ */
74
+ const isRefreshReady = isDateAfter({
75
+ fullDate: calculateRelativeDate(now, this.config.sessionRefreshThreshold || {
76
+ minutes: 5,
77
+ }),
78
+ relativeTo: userIdResult.jwtExpiration,
79
+ });
80
+ if (isRefreshReady) {
81
+ return this.createLoginHeaders({
82
+ requestHeaders: {},
83
+ userId: userIdResult.userId,
84
+ isSignUpCookie: userIdResult.cookieName === AuthCookieName.SignUp,
85
+ });
86
+ }
87
+ else {
88
+ return undefined;
89
+ }
90
+ }
91
+ /** Reads the user's assumed user headers and, if configured, gets the assumed user. */
92
+ async getAssumedUser({ headers, originalUserId, }) {
93
+ if (!originalUserId ||
94
+ !this.config.assumeUser ||
95
+ !(await this.config.assumeUser.canAssumeUser({
96
+ userId: originalUserId,
97
+ }))) {
98
+ return undefined;
99
+ }
100
+ const assumedUserHeader = ensureArray(headers[this.config.overrides?.assumedUserHeaderName || AuthHeaderName.AssumedUser])[0];
101
+ if (!assumedUserHeader) {
102
+ return undefined;
103
+ }
104
+ const parsedAssumedUserData = await this.config.assumeUser.handleAssumedUserData(assumedUserHeader);
105
+ if (!parsedAssumedUserData || !parsedAssumedUserData.userId) {
106
+ return undefined;
107
+ }
108
+ const assumedUser = await this.getDatabaseUser({
109
+ isSignUpCookie: false,
110
+ userId: parsedAssumedUserData.userId,
111
+ assumedUserParams: parsedAssumedUserData.assumedUserParams,
112
+ });
113
+ return assumedUser;
114
+ }
115
+ /** Securely extract a user from their request headers. */
116
+ async getSecureUser({ requestHeaders, isSignUpCookie, }) {
117
+ const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth, this.config.overrides);
118
+ if (!userIdResult) {
119
+ return undefined;
120
+ }
121
+ const user = await this.getDatabaseUser({
122
+ userId: userIdResult.userId,
123
+ assumedUserParams: undefined,
124
+ isSignUpCookie: !!isSignUpCookie,
125
+ });
126
+ if (!user) {
127
+ return undefined;
128
+ }
129
+ const assumedUser = await this.getAssumedUser({
130
+ headers: requestHeaders,
131
+ originalUserId: userIdResult.userId,
132
+ });
133
+ const cookieRefreshHeaders = (await this.createCookieRefreshHeaders({
134
+ userIdResult,
135
+ })) || {};
136
+ if (assumedUser) {
137
+ return {
138
+ user: assumedUser,
139
+ isAssumed: true,
140
+ responseHeaders: cookieRefreshHeaders,
141
+ };
142
+ }
143
+ else {
144
+ return {
145
+ user,
146
+ isAssumed: false,
147
+ responseHeaders: cookieRefreshHeaders,
148
+ };
149
+ }
150
+ }
151
+ /**
152
+ * Get all the JWT params used when creating the auth cookie, in case you need them for
153
+ * something else too.
154
+ */
155
+ async getJwtParams() {
156
+ const rawJwtKeys = await this.config.getJetKeys();
157
+ const cacheKey = JSON.stringify(rawJwtKeys);
158
+ const cachedParsedKeys = this.cachedParsedJwtKeys[cacheKey];
159
+ const parsedKeys = cachedParsedKeys ?? (await parseJwtKeys(rawJwtKeys));
160
+ if (!cachedParsedKeys) {
161
+ this.cachedParsedJwtKeys = { [cacheKey]: parsedKeys };
162
+ }
163
+ return {
164
+ jwtKeys: parsedKeys,
165
+ audience: 'server-context',
166
+ issuer: 'server-auth',
167
+ jwtDuration: this.config.userSessionIdleTimeout || defaultSessionIdleTimeout,
168
+ };
169
+ }
170
+ /** Use these headers to log out the user. */
171
+ async createLogoutHeaders() {
172
+ const signUpCookieHeaders = generateLogoutHeaders(await this.getCookieParams({
173
+ isSignUpCookie: true,
174
+ }), this.config.overrides);
175
+ const authCookieHeaders = generateLogoutHeaders(await this.getCookieParams({
176
+ isSignUpCookie: false,
177
+ }), this.config.overrides);
178
+ return {
179
+ ...authCookieHeaders,
180
+ 'set-cookie': mergeHeaderValues(signUpCookieHeaders['set-cookie'], authCookieHeaders['set-cookie']),
181
+ };
182
+ }
183
+ /** Use these headers to log a user in. */
184
+ async createLoginHeaders({ userId, requestHeaders, isSignUpCookie, }) {
185
+ const oppositeCookieName = isSignUpCookie ? AuthCookieName.Auth : AuthCookieName.SignUp;
186
+ const hasExistingOppositeCookie = requestHeaders.cookie?.includes(`${oppositeCookieName}=`);
187
+ const discardOppositeCookieHeaders = hasExistingOppositeCookie
188
+ ? generateLogoutHeaders(await this.getCookieParams({
189
+ isSignUpCookie: !isSignUpCookie,
190
+ }), this.config.overrides)
191
+ : undefined;
192
+ const newCookieHeaders = await generateSuccessfulLoginHeaders(userId, await this.getCookieParams({
193
+ isSignUpCookie,
194
+ }), this.config.overrides);
195
+ return {
196
+ ...newCookieHeaders,
197
+ 'set-cookie': mergeHeaderValues(newCookieHeaders['set-cookie'], discardOppositeCookieHeaders?.['set-cookie']),
198
+ ...(isSignUpCookie
199
+ ? {
200
+ [AuthHeaderName.IsSignUpAuth]: 'true',
201
+ }
202
+ : {}),
203
+ };
204
+ }
205
+ /**
206
+ * @deprecated This only half authenticates the user. It should only be used in circumstances
207
+ * where JavaScript cannot be used to attach the CSRF token header to the request (like when
208
+ * opening a PDF file). Use `.getSecureUser()` instead, whenever possible.
209
+ */
210
+ async getInsecureUser({ headers, }) {
211
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
212
+ const userIdResult = await insecureExtractUserIdFromCookieAlone(headers, await this.getJwtParams(), AuthCookieName.Auth);
213
+ if (!userIdResult) {
214
+ return undefined;
215
+ }
216
+ const user = await this.getDatabaseUser({
217
+ isSignUpCookie: false,
218
+ userId: userIdResult.userId,
219
+ assumedUserParams: undefined,
220
+ });
221
+ if (!user) {
222
+ return undefined;
223
+ }
224
+ return {
225
+ user,
226
+ isAssumed: false,
227
+ responseHeaders: (await this.createCookieRefreshHeaders({
228
+ userIdResult,
229
+ })) || {},
230
+ };
231
+ }
232
+ }
@@ -0,0 +1,64 @@
1
+ import { type JsonCompatibleObject, type MaybePromise, type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
2
+ import { type EmptyObject } from 'type-fest';
3
+ /**
4
+ * Config for {@link FrontendAuthClient}.
5
+ *
6
+ * @category Internal
7
+ */
8
+ export type FrontendAuthClientConfig = PartialWithUndefined<{
9
+ /**
10
+ * Determine if the current user can assume the identity of another user. If this is not
11
+ * defined, all users will be blocked from assuming other user identities.
12
+ */
13
+ canAssumeUser: () => MaybePromise<boolean>;
14
+ /** Called whenever the current user becomes unauthorized and their CSRF token is wiped. */
15
+ authClearedCallback: () => MaybePromise<void>;
16
+ overrides: PartialWithUndefined<{
17
+ localStorage: Pick<Storage, 'setItem' | 'removeItem' | 'getItem'>;
18
+ csrfHeaderName: string;
19
+ assumedUserHeaderName: string;
20
+ }>;
21
+ }>;
22
+ /**
23
+ * An auth client for sending and validating client requests to a backend. This should only be used
24
+ * in a frontend environment as it accesses native browser APIs.
25
+ *
26
+ * @category Auth : Client
27
+ * @category Client
28
+ */
29
+ export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
30
+ protected readonly config: FrontendAuthClientConfig;
31
+ constructor(config?: FrontendAuthClientConfig);
32
+ /** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
33
+ getCurrentCsrfToken(): Promise<string | undefined>;
34
+ /** @returns Whether the user assuming succeeded or not. */
35
+ assumeUser(assumedUserParams: Readonly<AssumedUserParams>): Promise<boolean>;
36
+ getAssumedUser(): AssumedUserParams | undefined;
37
+ /**
38
+ * Creates a `RequestInit` object for the `fetch` API. If you have other request init options,
39
+ * use [`mergeDeep` from
40
+ * `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
41
+ * combine them with these.
42
+ */
43
+ createAuthenticatedRequestInit(): Promise<RequestInit>;
44
+ /** Wipes the current user auth. */
45
+ logout(): Promise<void>;
46
+ /**
47
+ * Use to handle a login response. Automatically stores the CSRF token.
48
+ *
49
+ * @throws Error if the login response failed.
50
+ * @throws Error if the login response has an invalid CSRF token.
51
+ */
52
+ handleLoginResponse(response: Readonly<SelectFrom<Response, {
53
+ headers: true;
54
+ ok: true;
55
+ }>>): Promise<void>;
56
+ /**
57
+ * Use to verify _all_ responses received from the backend. Immediately logs the user out once
58
+ * an unauthorized response is detected.
59
+ */
60
+ verifyResponseAuth(response: Readonly<SelectFrom<Response, {
61
+ status: true;
62
+ headers: true;
63
+ }>>): Promise<void>;
64
+ }