auth-vir 1.3.2 → 2.0.1
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/README.md +27 -18
- package/dist/auth-client/backend-auth.client.d.ts +177 -0
- package/dist/auth-client/backend-auth.client.js +232 -0
- package/dist/auth-client/frontend-auth.client.d.ts +65 -0
- package/dist/auth-client/frontend-auth.client.js +108 -0
- package/dist/auth.d.ts +32 -46
- package/dist/auth.js +36 -36
- package/dist/cookie.d.ts +15 -4
- package/dist/cookie.js +16 -4
- package/dist/csrf-token.d.ts +113 -3
- package/dist/csrf-token.js +101 -5
- package/dist/generated/browser.d.ts +9 -0
- package/dist/generated/browser.js +16 -0
- package/dist/generated/client.d.ts +26 -0
- package/dist/generated/client.js +31 -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 +9 -0
- package/dist/generated/internal/class.d.ts +126 -0
- package/dist/generated/internal/class.js +84 -0
- package/dist/generated/internal/prismaNamespace.d.ts +544 -0
- package/dist/generated/internal/prismaNamespace.js +101 -0
- package/dist/generated/internal/prismaNamespaceBrowser.d.ts +75 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +69 -0
- package/dist/generated/models/User.d.ts +983 -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/headers.d.ts +20 -0
- package/dist/headers.js +33 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/jwt/jwt.d.ts +11 -2
- package/dist/jwt/jwt.js +14 -3
- package/dist/jwt/user-jwt.d.ts +8 -8
- package/dist/jwt/user-jwt.js +12 -9
- package/package.json +12 -7
- package/src/auth-client/backend-auth.client.ts +500 -0
- package/src/auth-client/frontend-auth.client.ts +183 -0
- package/src/auth.ts +99 -77
- package/src/cookie.ts +20 -8
- package/src/csrf-token.ts +196 -5
- package/src/generated/browser.ts +23 -0
- package/src/generated/client.ts +47 -0
- package/src/generated/commonInputTypes.ts +147 -0
- package/src/generated/enums.ts +14 -0
- package/src/generated/internal/class.ts +236 -0
- package/src/generated/internal/prismaNamespace.ts +761 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +102 -0
- package/src/generated/models/User.ts +1135 -0
- package/src/generated/models.ts +11 -0
- package/src/generated/shapes.gen.ts +15 -0
- package/src/headers.ts +35 -0
- package/src/index.ts +3 -0
- package/src/jwt/jwt.ts +34 -5
- package/src/jwt/user-jwt.ts +21 -12
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 [`
|
|
53
|
-
1. Set `customHeaders: [
|
|
54
|
-
2. Set the header `Access-Control-Allow-Headers` to (at least) `
|
|
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 =
|
|
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:
|
|
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: {[
|
|
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
|
-
|
|
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
|
-
[
|
|
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 [`
|
|
314
|
-
1. Set `customHeaders: [
|
|
315
|
-
2. Set the header `Access-Control-Allow-Headers` to (at least) `
|
|
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,65 @@
|
|
|
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
|
+
/** Gets the assumed user params stored in local storage, if any. */
|
|
37
|
+
getAssumedUser(): AssumedUserParams | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Creates a `RequestInit` object for the `fetch` API. If you have other request init options,
|
|
40
|
+
* use [`mergeDeep` from
|
|
41
|
+
* `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
|
|
42
|
+
* combine them with these.
|
|
43
|
+
*/
|
|
44
|
+
createAuthenticatedRequestInit(): Promise<RequestInit>;
|
|
45
|
+
/** Wipes the current user auth. */
|
|
46
|
+
logout(): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Use to handle a login response. Automatically stores the CSRF token.
|
|
49
|
+
*
|
|
50
|
+
* @throws Error if the login response failed.
|
|
51
|
+
* @throws Error if the login response has an invalid CSRF token.
|
|
52
|
+
*/
|
|
53
|
+
handleLoginResponse(response: Readonly<SelectFrom<Response, {
|
|
54
|
+
headers: true;
|
|
55
|
+
ok: true;
|
|
56
|
+
}>>): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Use to verify _all_ responses received from the backend. Immediately logs the user out once
|
|
59
|
+
* an unauthorized response is detected.
|
|
60
|
+
*/
|
|
61
|
+
verifyResponseAuth(response: Readonly<SelectFrom<Response, {
|
|
62
|
+
status: true;
|
|
63
|
+
headers: true;
|
|
64
|
+
}>>): Promise<void>;
|
|
65
|
+
}
|