@devmoods/express-extras 0.73.0 → 0.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/emailAndPassword.js +14 -8
- package/dist/auth/google-client.d.ts +12 -0
- package/dist/auth/google-client.js +27 -0
- package/dist/auth/google.d.ts +2 -1
- package/dist/auth/google.js +34 -21
- package/dist/auth/passwordless.d.ts +1 -1
- package/dist/auth/passwordless.js +23 -11
- package/dist/auth/token.d.ts +2 -2
- package/dist/auth/token.js +3 -3
- package/dist/auth/tokenStorage.d.ts +1 -1
- package/dist/auth/tokenStorage.js +1 -1
- package/dist/auth/types.d.ts +6 -2
- package/dist/auth.d.ts +4 -4
- package/dist/auth.js +50 -10
- package/dist/cache.d.ts +27 -6
- package/dist/cache.js +129 -66
- package/dist/composeMiddleware.js +2 -2
- package/dist/config.d.ts +3 -1
- package/dist/config.js +1 -0
- package/dist/context.d.ts +1 -1
- package/dist/context.js +2 -2
- package/dist/cookie-consent.d.ts +1 -1
- package/dist/csrf.d.ts +1 -1
- package/dist/csrf.js +4 -0
- package/dist/email.d.ts +1 -1
- package/dist/faktory.d.ts +1 -1
- package/dist/listen.js +1 -2
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +2 -2
- package/dist/manage.js +3 -3
- package/dist/middleware.d.ts +1 -1
- package/dist/middleware.js +81 -15
- package/dist/rate-limit.d.ts +1 -1
- package/dist/redis.d.ts +1 -1
- package/dist/routes.d.ts +2 -2
- package/dist/slack.d.ts +1 -1
- package/dist/utils.d.ts +0 -3
- package/dist/utils.js +0 -1
- package/dist/vite/defineViteConfig.d.ts +1 -1
- package/dist/vite/ssr.d.ts +1 -1
- package/dist/vite/ssr.js +1 -1
- package/dist/vitest/testClient.d.ts +5 -5
- package/package.json +15 -16
|
@@ -6,7 +6,7 @@ import {} from './types.js';
|
|
|
6
6
|
import { Unauthorized } from '../errors.js';
|
|
7
7
|
import { getLogger } from '../logger.js';
|
|
8
8
|
import { jsonSchema } from '../validate.js';
|
|
9
|
-
const passwordSchema = { type: 'string', minLength:
|
|
9
|
+
const passwordSchema = { type: 'string', minLength: 8 };
|
|
10
10
|
const emailSchema = { type: 'string', minLength: 4 };
|
|
11
11
|
const logger = getLogger('auth');
|
|
12
12
|
export function EmailAndPasswordAuthProvider(options) {
|
|
@@ -35,7 +35,7 @@ export function EmailAndPasswordAuthProvider(options) {
|
|
|
35
35
|
catch (error) {
|
|
36
36
|
logger.error(error);
|
|
37
37
|
await context.jitter(timer.getDuration());
|
|
38
|
-
throw Unauthorized(
|
|
38
|
+
throw Unauthorized('Invalid email or password', { cause: error });
|
|
39
39
|
}
|
|
40
40
|
});
|
|
41
41
|
router.post('/change-password', {
|
|
@@ -73,11 +73,18 @@ export function EmailAndPasswordAuthProvider(options) {
|
|
|
73
73
|
}, async (req) => {
|
|
74
74
|
const { email } = req.body;
|
|
75
75
|
const timer = startTimer();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
try {
|
|
77
|
+
const user = await options.login(email, skipPasswordCheck);
|
|
78
|
+
const temporaryToken = await context.createTemporaryToken(user);
|
|
79
|
+
await options.onEmailVerificationRequest?.(user, temporaryToken);
|
|
80
|
+
await context.jitter(timer.getDuration());
|
|
81
|
+
return 202;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
logger.error(error);
|
|
85
|
+
await context.jitter(timer.getDuration());
|
|
86
|
+
return 202;
|
|
87
|
+
}
|
|
81
88
|
});
|
|
82
89
|
router.post('/verify-email', {
|
|
83
90
|
request: jsonSchema({
|
|
@@ -93,7 +100,6 @@ export function EmailAndPasswordAuthProvider(options) {
|
|
|
93
100
|
}
|
|
94
101
|
catch (error) {
|
|
95
102
|
logger.warn('failed to verify token', {
|
|
96
|
-
token,
|
|
97
103
|
user: context.filterUser(user),
|
|
98
104
|
error: error.message,
|
|
99
105
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function exchangeGoogleAuthorizationCode({ code, clientId, clientSecret, redirectUri, }: {
|
|
2
|
+
code: string;
|
|
3
|
+
clientId: string;
|
|
4
|
+
clientSecret: string;
|
|
5
|
+
redirectUri: string;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
id_token?: string | null;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function verifyGoogleIdToken({ token, clientId, }: {
|
|
10
|
+
token: string;
|
|
11
|
+
clientId: string;
|
|
12
|
+
}): Promise<import("jose").JWTPayload>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createFetch } from '@devmoods/fetch';
|
|
2
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
3
|
+
const fetch = createFetch();
|
|
4
|
+
const googleJwks = createRemoteJWKSet(new URL('https://www.googleapis.com/oauth2/v3/certs'));
|
|
5
|
+
export async function exchangeGoogleAuthorizationCode({ code, clientId, clientSecret, redirectUri, }) {
|
|
6
|
+
const response = await fetch('https://oauth2.googleapis.com/token', {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: {
|
|
9
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
10
|
+
},
|
|
11
|
+
body: new URLSearchParams({
|
|
12
|
+
code,
|
|
13
|
+
client_id: clientId,
|
|
14
|
+
client_secret: clientSecret,
|
|
15
|
+
redirect_uri: redirectUri,
|
|
16
|
+
grant_type: 'authorization_code',
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
return response.jsonData;
|
|
20
|
+
}
|
|
21
|
+
export async function verifyGoogleIdToken({ token, clientId, }) {
|
|
22
|
+
const { payload } = await jwtVerify(token, googleJwks, {
|
|
23
|
+
audience: clientId,
|
|
24
|
+
issuer: ['https://accounts.google.com', 'accounts.google.com'],
|
|
25
|
+
});
|
|
26
|
+
return payload;
|
|
27
|
+
}
|
package/dist/auth/google.d.ts
CHANGED
|
@@ -7,7 +7,8 @@ interface GoogleAuthProviderOptions<UserModel = AnyUser> {
|
|
|
7
7
|
clientId?: string;
|
|
8
8
|
clientSecret?: string;
|
|
9
9
|
redirectUri?: string;
|
|
10
|
+
hostedDomain?: string;
|
|
10
11
|
getOrCreateUserFromUserIdentifier(userIdentifier: string, profile: GoogleProfile): Promise<UserModel>;
|
|
11
12
|
}
|
|
12
|
-
export declare function GoogleAuthProvider<UserModel extends AnyUser>({ clientId, clientSecret, redirectUri, getOrCreateUserFromUserIdentifier }: GoogleAuthProviderOptions<UserModel>): AuthProvider<UserModel>;
|
|
13
|
+
export declare function GoogleAuthProvider<UserModel extends AnyUser>({ clientId, clientSecret, redirectUri, hostedDomain, getOrCreateUserFromUserIdentifier, }: GoogleAuthProviderOptions<UserModel>): AuthProvider<UserModel>;
|
|
13
14
|
export {};
|
package/dist/auth/google.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { config } from '@devmoods/config';
|
|
2
2
|
import { type } from 'arktype';
|
|
3
|
-
import {
|
|
4
|
-
import { BadRequest, Unauthorized } from '../errors.js';
|
|
5
|
-
import { getLogger } from '../logger.js';
|
|
3
|
+
import { exchangeGoogleAuthorizationCode, verifyGoogleIdToken, } from './google-client.js';
|
|
6
4
|
import { createTokenResponse } from './token.js';
|
|
7
5
|
import {} from './types.js';
|
|
6
|
+
import { BadRequest, Unauthorized } from '../errors.js';
|
|
7
|
+
import { getLogger } from '../logger.js';
|
|
8
8
|
const logger = getLogger('auth');
|
|
9
|
-
export function GoogleAuthProvider({ clientId = config.GOOGLE_AUTH_CLIENT_ID, clientSecret = config.GOOGLE_AUTH_CLIENT_SECRET, redirectUri = `${config.PUBLIC_URL}/api/v1/auth/google/callback`, getOrCreateUserFromUserIdentifier, }) {
|
|
10
|
-
const client = new OAuth2Client({
|
|
11
|
-
clientId,
|
|
12
|
-
clientSecret,
|
|
13
|
-
redirectUri,
|
|
14
|
-
});
|
|
9
|
+
export function GoogleAuthProvider({ clientId = config.GOOGLE_AUTH_CLIENT_ID, clientSecret = config.GOOGLE_AUTH_CLIENT_SECRET, redirectUri = `${config.PUBLIC_URL}/api/v1/auth/google/callback`, hostedDomain, getOrCreateUserFromUserIdentifier, }) {
|
|
15
10
|
const verifyAuthorizationCode = async (inputCode) => {
|
|
16
11
|
try {
|
|
17
|
-
const
|
|
12
|
+
const tokens = await exchangeGoogleAuthorizationCode({
|
|
13
|
+
code: inputCode,
|
|
14
|
+
clientId,
|
|
15
|
+
clientSecret,
|
|
16
|
+
redirectUri,
|
|
17
|
+
});
|
|
18
18
|
return { idToken: tokens.id_token };
|
|
19
19
|
}
|
|
20
20
|
catch (e) {
|
|
@@ -23,15 +23,18 @@ export function GoogleAuthProvider({ clientId = config.GOOGLE_AUTH_CLIENT_ID, cl
|
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
const verifyIdToken = async (token) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
try {
|
|
27
|
+
return await verifyGoogleIdToken({
|
|
28
|
+
token,
|
|
29
|
+
clientId,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
logger.error(e);
|
|
34
|
+
throw Unauthorized('Token from Google contains invalid payload', {
|
|
35
|
+
cause: e,
|
|
36
|
+
});
|
|
33
37
|
}
|
|
34
|
-
return payload;
|
|
35
38
|
};
|
|
36
39
|
const requiredScopes = ['openid', 'email', 'profile'];
|
|
37
40
|
return {
|
|
@@ -79,10 +82,20 @@ export function GoogleAuthProvider({ clientId = config.GOOGLE_AUTH_CLIENT_ID, cl
|
|
|
79
82
|
throw Unauthorized('Invalid id token');
|
|
80
83
|
}
|
|
81
84
|
const payload = await verifyIdToken(idToken);
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
if (typeof payload.sub !== 'string' || payload.sub.length === 0) {
|
|
86
|
+
throw Unauthorized('Token from Google is missing subject');
|
|
87
|
+
}
|
|
88
|
+
if (typeof payload.email !== 'string' ||
|
|
89
|
+
payload.email_verified !== true) {
|
|
90
|
+
throw Unauthorized('Token from Google contains unverified email');
|
|
91
|
+
}
|
|
92
|
+
if (hostedDomain &&
|
|
93
|
+
(typeof payload.hd !== 'string' || payload.hd !== hostedDomain)) {
|
|
94
|
+
throw Unauthorized('Token from Google contains invalid hosted domain');
|
|
95
|
+
}
|
|
96
|
+
const user = await getOrCreateUserFromUserIdentifier(payload.sub, {
|
|
84
97
|
email: payload.email,
|
|
85
|
-
fullName: payload.name,
|
|
98
|
+
fullName: typeof payload.name === 'string' ? payload.name : '',
|
|
86
99
|
});
|
|
87
100
|
const accessToken = await context.login(req, res, user);
|
|
88
101
|
logger.info('google-login', { email: user.email });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AnyUser, type AuthProvider } from './types.js';
|
|
2
2
|
export interface PasswordlessAuthProviderOptions<UserModel = AnyUser> {
|
|
3
3
|
getUserByEmail(email: string): Promise<UserModel | null>;
|
|
4
|
-
onLoginRequest(user: UserModel, temporaryToken: string, otp: string): Promise<void>;
|
|
4
|
+
onLoginRequest(user: UserModel, challengeId: string, temporaryToken: string, otp: string): Promise<void>;
|
|
5
5
|
}
|
|
6
6
|
export declare function PasswordlessAuthProvider<UserModel extends AnyUser>(options: PasswordlessAuthProviderOptions<UserModel>): AuthProvider<UserModel>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
1
2
|
import { DoesNotExistError } from '@devmoods/postgres';
|
|
2
3
|
import { createTokenResponse } from './token.js';
|
|
3
4
|
import {} from './types.js';
|
|
5
|
+
import { BadRequest } from '../errors.js';
|
|
4
6
|
import { getLogger } from '../logger.js';
|
|
5
7
|
import { jsonSchema } from '../validate.js';
|
|
6
8
|
const logger = getLogger('auth');
|
|
@@ -25,20 +27,30 @@ export function PasswordlessAuthProvider(options) {
|
|
|
25
27
|
type: 'object',
|
|
26
28
|
properties: {
|
|
27
29
|
type: { type: 'string', enum: ['otp', 'token'] },
|
|
30
|
+
challengeId: { type: 'string' },
|
|
28
31
|
otp: { type: 'string' },
|
|
29
32
|
token: { type: 'string' },
|
|
30
33
|
},
|
|
31
34
|
required: ['type'],
|
|
32
35
|
}),
|
|
33
36
|
}, async (req, res) => {
|
|
34
|
-
const { token: temporaryToken, otp, type: tokenType } = req.body;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const { challengeId, token: temporaryToken, otp, type: tokenType, } = req.body;
|
|
38
|
+
if (tokenType === 'otp' && (!challengeId || !otp)) {
|
|
39
|
+
throw BadRequest('OTP logins require challengeId and otp');
|
|
40
|
+
}
|
|
41
|
+
if (tokenType === 'token' && !temporaryToken) {
|
|
42
|
+
throw BadRequest('Token logins require token');
|
|
43
|
+
}
|
|
44
|
+
const uid = tokenType === 'otp'
|
|
45
|
+
? await context.verifyOtp(String(challengeId), String(otp), null, false)
|
|
46
|
+
: await context.verifyTemporaryToken(String(temporaryToken), null, false);
|
|
40
47
|
const user = await context.getUserById(uid);
|
|
41
|
-
|
|
48
|
+
if (tokenType === 'otp') {
|
|
49
|
+
await context.verifyOtp(String(challengeId), String(otp), user);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
await context.verifyTemporaryToken(String(temporaryToken), user);
|
|
53
|
+
}
|
|
42
54
|
const accessToken = await context.login(req, res, user);
|
|
43
55
|
logger.info('passwordless login', { tokenType, uid: user.id });
|
|
44
56
|
if (req.accepts('html')) {
|
|
@@ -62,13 +74,13 @@ export function PasswordlessAuthProvider(options) {
|
|
|
62
74
|
const user = await getUserOrNull(email);
|
|
63
75
|
if (!user) {
|
|
64
76
|
logger.warn('passwordless: user not found', { email });
|
|
65
|
-
return 202;
|
|
77
|
+
return [202, { challengeId: crypto.randomUUID() }];
|
|
66
78
|
}
|
|
67
79
|
const token = await context.createTemporaryToken(user);
|
|
68
|
-
const otp = await context.createOtp(user);
|
|
69
|
-
await options.onLoginRequest(user, token, otp);
|
|
80
|
+
const { challengeId, otp } = await context.createOtp(user);
|
|
81
|
+
await options.onLoginRequest(user, challengeId, token, otp);
|
|
70
82
|
logger.info('passwordless login requested', { email });
|
|
71
|
-
return 202;
|
|
83
|
+
return [202, { challengeId }];
|
|
72
84
|
});
|
|
73
85
|
},
|
|
74
86
|
};
|
package/dist/auth/token.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare const tokenSchema: ValidationSchema;
|
|
|
4
4
|
export declare function createTokenResponse(accessToken: AccessToken): readonly [201, {
|
|
5
5
|
readonly token: string;
|
|
6
6
|
}, {
|
|
7
|
-
readonly 'Cache-Control':
|
|
7
|
+
readonly 'Cache-Control': "no-store";
|
|
8
8
|
}];
|
|
9
9
|
export type AccessToken = Awaited<ReturnType<typeof createAccessToken>>;
|
|
10
10
|
/**
|
|
@@ -12,7 +12,7 @@ export type AccessToken = Awaited<ReturnType<typeof createAccessToken>>;
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function createAccessToken(payload: any, version?: string, secret?: string): Promise<{
|
|
14
14
|
token: string;
|
|
15
|
-
tokenType:
|
|
15
|
+
tokenType: "Bearer";
|
|
16
16
|
expiresIn: number;
|
|
17
17
|
}>;
|
|
18
18
|
/**
|
package/dist/auth/token.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { config } from '@devmoods/config';
|
|
2
3
|
import {} from 'express';
|
|
3
4
|
import { SignJWT, jwtVerify } from 'jose';
|
|
4
|
-
import { v4 } from 'uuid';
|
|
5
5
|
import { InvalidToken } from './errors.js';
|
|
6
6
|
import {} from '../validate.js';
|
|
7
7
|
const ACCESS_TOKEN_COOKIE_NAME = 'access_token';
|
|
@@ -27,7 +27,7 @@ export function createTokenResponse(accessToken) {
|
|
|
27
27
|
export async function createAccessToken(payload, version = '1', secret = config.AUTH_JWT_SECRET) {
|
|
28
28
|
const expiresIn = config.AUTH_TOKEN_EXPIRES;
|
|
29
29
|
const secretKey = new TextEncoder().encode(secret);
|
|
30
|
-
const token = await new SignJWT({ v: version, ...payload })
|
|
30
|
+
const token = await new SignJWT({ jti: randomUUID(), v: version, ...payload })
|
|
31
31
|
.setProtectedHeader({ alg: 'HS256' })
|
|
32
32
|
.setExpirationTime(`${expiresIn}s`)
|
|
33
33
|
.setIssuedAt(new Date())
|
|
@@ -100,7 +100,7 @@ export function extractAccessTokenFromRequest(req) {
|
|
|
100
100
|
export function setAccessTokenCookie(res, token) {
|
|
101
101
|
const sameSite = config.AUTH_COOKIE_SAME_SITE;
|
|
102
102
|
if (token == null) {
|
|
103
|
-
res.cookie(ACCESS_TOKEN_COOKIE_NAME,
|
|
103
|
+
res.cookie(ACCESS_TOKEN_COOKIE_NAME, randomUUID(), {
|
|
104
104
|
expires: new Date(1970, 0, 1),
|
|
105
105
|
sameSite,
|
|
106
106
|
secure: process.env.NODE_ENV === 'production',
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -31,6 +31,10 @@ export interface DecodedTemporaryToken {
|
|
|
31
31
|
};
|
|
32
32
|
signature: string;
|
|
33
33
|
}
|
|
34
|
+
export interface OtpChallenge {
|
|
35
|
+
challengeId: string;
|
|
36
|
+
otp: string;
|
|
37
|
+
}
|
|
34
38
|
export interface AuthProviderContext<UserModel extends AnyUser> {
|
|
35
39
|
filterUser: AuthOptions<UserModel>['filterUser'];
|
|
36
40
|
tokenStorage: AuthOptions<UserModel>['tokenStorage'];
|
|
@@ -38,8 +42,8 @@ export interface AuthProviderContext<UserModel extends AnyUser> {
|
|
|
38
42
|
createTemporaryToken(user: UserModel | null): Promise<string>;
|
|
39
43
|
verifyTemporaryToken(token: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
40
44
|
decodeTemporaryToken(token: string): DecodedTemporaryToken;
|
|
41
|
-
createOtp(user: UserModel): Promise<
|
|
42
|
-
verifyOtp(otp: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
45
|
+
createOtp(user: UserModel): Promise<OtpChallenge>;
|
|
46
|
+
verifyOtp(challengeId: string, otp: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
43
47
|
middleware: (options?: AuthMiddlewareOptions<UserModel>) => RequestHandler;
|
|
44
48
|
login: (req: Request, res: Response, user: UserModel, isRefresh?: boolean) => Promise<AccessToken>;
|
|
45
49
|
getUserById(id: string): Promise<UserModel>;
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type RequestHandler, type Response, type Request, type Router } from 'express';
|
|
2
|
-
import { type AnyUser, type AuthIsAllowedFn, type AuthMiddlewareOptions, type AuthOptions, type DecodedTemporaryToken } from './auth/types.js';
|
|
2
|
+
import { type AnyUser, type AuthIsAllowedFn, type AuthMiddlewareOptions, type AuthOptions, type DecodedTemporaryToken, type OtpChallenge } from './auth/types.js';
|
|
3
3
|
export * from './auth/types.js';
|
|
4
4
|
export * from './auth/emailAndPassword.js';
|
|
5
5
|
export * from './auth/passwordless.js';
|
|
@@ -15,9 +15,9 @@ export interface CreateAuth<UserModel extends AnyUser> {
|
|
|
15
15
|
createTemporaryToken(user: UserModel | null): Promise<string>;
|
|
16
16
|
verifyTemporaryToken(token: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
17
17
|
decodeTemporaryToken(token: string): DecodedTemporaryToken;
|
|
18
|
-
createOtp(user: UserModel): Promise<
|
|
19
|
-
verifyOtp(otp: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
18
|
+
createOtp(user: UserModel): Promise<OtpChallenge>;
|
|
19
|
+
verifyOtp(challengeId: string, otp: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
20
20
|
logout: (req: Request, res: Response) => Promise<void>;
|
|
21
21
|
setSessionTokenVersion: (version: string) => void;
|
|
22
22
|
}
|
|
23
|
-
export declare function createAuth<UserModel extends AnyUser>({ getUserById, filterUser, tokenStorage, providers, loginUrl, onUserLoggedIn, sessionTokenVersion: initialSessionTokenVersion }: AuthOptions<UserModel>): CreateAuth<UserModel>;
|
|
23
|
+
export declare function createAuth<UserModel extends AnyUser>({ getUserById, filterUser, tokenStorage, providers, loginUrl, onUserLoggedIn, sessionTokenVersion: initialSessionTokenVersion, }: AuthOptions<UserModel>): CreateAuth<UserModel>;
|
package/dist/auth.js
CHANGED
|
@@ -17,14 +17,27 @@ export * from './auth/token.js';
|
|
|
17
17
|
export * from './auth/google.js';
|
|
18
18
|
export * from './auth/tokenStorage.js';
|
|
19
19
|
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
|
20
|
+
const hashEquals = (left, right) => {
|
|
21
|
+
const leftBuffer = Buffer.from(left, 'hex');
|
|
22
|
+
const rightBuffer = Buffer.from(right, 'hex');
|
|
23
|
+
if (leftBuffer.length !== rightBuffer.length) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return crypto.timingSafeEqual(leftBuffer, rightBuffer);
|
|
27
|
+
};
|
|
20
28
|
export function createAuth({ getUserById, filterUser, tokenStorage, providers, loginUrl = '/', onUserLoggedIn, sessionTokenVersion: initialSessionTokenVersion = '1', }) {
|
|
21
29
|
let sessionTokenVersion = initialSessionTokenVersion;
|
|
22
30
|
const useCurrentUser = () => useRequestContext('currentUser');
|
|
23
31
|
const setCurrentUser = (user) => setRequestContext('currentUser', user);
|
|
24
32
|
const login = async (req, res, user, isRefresh = false) => {
|
|
33
|
+
const previousToken = isRefresh ? extractAccessTokenFromRequest(req) : null;
|
|
34
|
+
const previousSessionId = previousToken ? sha256(previousToken) : null;
|
|
25
35
|
const accessToken = await createAccessToken({ uid: user.id }, sessionTokenVersion);
|
|
26
36
|
const sessionId = sha256(accessToken.token);
|
|
27
37
|
await tokenStorage.set(sessionId, String(user.id), accessToken.expiresIn);
|
|
38
|
+
if (previousSessionId && previousSessionId !== sessionId) {
|
|
39
|
+
await tokenStorage.invalidate(previousSessionId);
|
|
40
|
+
}
|
|
28
41
|
setAccessTokenCookie(res, accessToken.token);
|
|
29
42
|
setCurrentUser(user);
|
|
30
43
|
if (!isRefresh) {
|
|
@@ -161,7 +174,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
161
174
|
if (req.accepts('html')) {
|
|
162
175
|
return res.redirect(loginUrl);
|
|
163
176
|
}
|
|
164
|
-
return next(Unauthorized(
|
|
177
|
+
return next(Unauthorized('Invalid token', { cause: error }));
|
|
165
178
|
}
|
|
166
179
|
/* v8 ignore next -- @preserve */
|
|
167
180
|
next(error);
|
|
@@ -171,6 +184,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
171
184
|
function protect(isAuthorized) {
|
|
172
185
|
return async function authorizationMiddleware(req, res, next) {
|
|
173
186
|
try {
|
|
187
|
+
// oxlint-disable-next-line eslint-plugin-react-hooks/rules-of-hooks
|
|
174
188
|
const user = useCurrentUser();
|
|
175
189
|
if (!user) {
|
|
176
190
|
throw new InvalidToken('User not logged in');
|
|
@@ -202,22 +216,48 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
202
216
|
}
|
|
203
217
|
async function createOtp(user) {
|
|
204
218
|
const otp = crypto.randomInt(100000, 999999).toString();
|
|
205
|
-
|
|
206
|
-
|
|
219
|
+
const challengeId = crypto.randomUUID();
|
|
220
|
+
const otpHash = sha256(otp);
|
|
221
|
+
await tokenStorage.set(`otp:${challengeId}`, JSON.stringify({
|
|
222
|
+
uid: String(user.id),
|
|
223
|
+
otpHash,
|
|
224
|
+
attempts: 0,
|
|
225
|
+
maxAttempts: config.AUTH_OTP_MAX_ATTEMPTS,
|
|
226
|
+
}), config.AUTH_OTP_EXPIRES);
|
|
227
|
+
return { challengeId, otp };
|
|
207
228
|
}
|
|
208
|
-
async function verifyOtp(otp, user, consume = true) {
|
|
209
|
-
const
|
|
210
|
-
if (!
|
|
229
|
+
async function verifyOtp(challengeId, otp, user, consume = true) {
|
|
230
|
+
const storedChallenge = await tokenStorage.get(`otp:${challengeId}`);
|
|
231
|
+
if (!storedChallenge) {
|
|
211
232
|
throw new InvalidToken('Invalid OTP');
|
|
212
233
|
}
|
|
213
|
-
|
|
214
|
-
|
|
234
|
+
let parsedChallenge;
|
|
235
|
+
try {
|
|
236
|
+
parsedChallenge = JSON.parse(storedChallenge);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
await tokenStorage.invalidate(`otp:${challengeId}`);
|
|
240
|
+
throw new InvalidToken('Invalid OTP', { cause: error });
|
|
241
|
+
}
|
|
242
|
+
if (user && parsedChallenge.uid !== String(user.id)) {
|
|
243
|
+
throw new InvalidToken('Invalid OTP');
|
|
244
|
+
}
|
|
245
|
+
const attemptedOtpHash = sha256(otp);
|
|
246
|
+
const matches = hashEquals(parsedChallenge.otpHash, attemptedOtpHash);
|
|
247
|
+
if (!matches) {
|
|
248
|
+
const attempts = parsedChallenge.attempts + 1;
|
|
249
|
+
if (attempts >= parsedChallenge.maxAttempts) {
|
|
250
|
+
await tokenStorage.invalidate(`otp:${challengeId}`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
await tokenStorage.set(`otp:${challengeId}`, JSON.stringify({ ...parsedChallenge, attempts }), config.AUTH_OTP_EXPIRES);
|
|
254
|
+
}
|
|
215
255
|
throw new InvalidToken('Invalid OTP');
|
|
216
256
|
}
|
|
217
257
|
if (consume) {
|
|
218
|
-
await tokenStorage.invalidate(otp);
|
|
258
|
+
await tokenStorage.invalidate(`otp:${challengeId}`);
|
|
219
259
|
}
|
|
220
|
-
return
|
|
260
|
+
return parsedChallenge.uid;
|
|
221
261
|
}
|
|
222
262
|
/**
|
|
223
263
|
* Create a short-lived token tied to a user that can be
|
package/dist/cache.d.ts
CHANGED
|
@@ -3,16 +3,37 @@ export interface CacheOptions {
|
|
|
3
3
|
key?: string;
|
|
4
4
|
expiresMs?: number | null;
|
|
5
5
|
}
|
|
6
|
-
export
|
|
7
|
-
get(key: string): Promise<any>;
|
|
8
|
-
set(key: string, value: any, expires?: number | null | undefined): Promise<void>;
|
|
9
|
-
clear(key?: string): Promise<void>;
|
|
6
|
+
export declare abstract class CacheAdapter {
|
|
7
|
+
abstract get(key: string): Promise<any>;
|
|
8
|
+
abstract set(key: string, value: any, expires?: number | null | undefined): Promise<void>;
|
|
9
|
+
abstract clear(key?: string): Promise<void>;
|
|
10
10
|
}
|
|
11
11
|
export interface RedisCacheAdapterOptions {
|
|
12
12
|
keyPrefix?: string;
|
|
13
13
|
}
|
|
14
|
+
export interface MemoryCacheAdapterOptions {
|
|
15
|
+
maxSize?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare class RedisCacheAdapter extends CacheAdapter {
|
|
18
|
+
redis: RedisClient;
|
|
19
|
+
keyPrefix: string;
|
|
20
|
+
constructor(redis: RedisClient, { keyPrefix, }?: RedisCacheAdapterOptions);
|
|
21
|
+
get(key: string): Promise<any>;
|
|
22
|
+
set(key: string, value: unknown, expires?: number | null): Promise<void>;
|
|
23
|
+
clear(key?: string): Promise<void>;
|
|
24
|
+
private withKeyPrefix;
|
|
25
|
+
}
|
|
14
26
|
export declare function createRedisCacheAdapter(redis: RedisClient, options?: RedisCacheAdapterOptions): CacheAdapter;
|
|
15
|
-
export declare
|
|
27
|
+
export declare class MemoryCacheAdapter extends CacheAdapter {
|
|
28
|
+
#private;
|
|
29
|
+
maxSize?: number;
|
|
30
|
+
constructor({ maxSize }?: MemoryCacheAdapterOptions);
|
|
31
|
+
get(key: string): Promise<unknown>;
|
|
32
|
+
set(key: string, value: unknown, expires?: number): Promise<void>;
|
|
33
|
+
clear(key?: string): Promise<void>;
|
|
34
|
+
private evictLeastRecentlyUsedEntries;
|
|
35
|
+
}
|
|
36
|
+
export declare function createMemoryCacheAdapter(options?: MemoryCacheAdapterOptions): CacheAdapter;
|
|
16
37
|
export type Cache = ReturnType<typeof createCache>;
|
|
17
38
|
export declare function createCache(adapter: CacheAdapter): {
|
|
18
39
|
set: (key: string, value: any, expires?: number | null | undefined) => Promise<void>;
|
|
@@ -21,7 +42,7 @@ export declare function createCache(adapter: CacheAdapter): {
|
|
|
21
42
|
wrap: <T>(key: string, options: CacheOptions | number | null, fn: () => Promise<T>) => Promise<T>;
|
|
22
43
|
decorate: <T, Arguments extends any[]>(options: CacheOptions | number | null, fn: (...args: Arguments) => T | Promise<T>) => {
|
|
23
44
|
(...args: Arguments): Promise<T>;
|
|
24
|
-
clear
|
|
45
|
+
clear(...args: Arguments): Promise<void>;
|
|
25
46
|
};
|
|
26
47
|
hashKey: typeof hashKey;
|
|
27
48
|
};
|