@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.
@@ -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: 4 };
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(error.message, { cause: error });
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
- const user = await options.login(email, skipPasswordCheck);
77
- const temporaryToken = await context.createTemporaryToken(user);
78
- await options.onEmailVerificationRequest?.(user, temporaryToken);
79
- await context.jitter(timer.getDuration());
80
- return 202;
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
+ }
@@ -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 {};
@@ -1,20 +1,20 @@
1
1
  import { config } from '@devmoods/config';
2
2
  import { type } from 'arktype';
3
- import { OAuth2Client } from 'google-auth-library';
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 { tokens } = await client.getToken(inputCode);
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
- const ticket = await client.verifyIdToken({
27
- idToken: token,
28
- audience: clientId,
29
- });
30
- const payload = ticket.getPayload();
31
- if (!payload) {
32
- throw Unauthorized('Token from Google contains invalid payload');
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
- logger.info('google payload', { payload });
83
- const user = await getOrCreateUserFromUserIdentifier(payload?.sub, {
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
- const verify = tokenType === 'otp'
36
- ? context.verifyOtp
37
- : context.verifyTemporaryToken;
38
- const tokenValue = String(tokenType === 'otp' ? otp : temporaryToken);
39
- const uid = await verify(tokenValue, null, false);
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
- await verify(tokenValue, user);
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
  };
@@ -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': 'no-store';
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: 'Bearer';
15
+ tokenType: "Bearer";
16
16
  expiresIn: number;
17
17
  }>;
18
18
  /**
@@ -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, v4(), {
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',
@@ -1,5 +1,5 @@
1
- import { type RedisClient } from '../redis.js';
2
1
  import { TokenStorage } from './types.js';
2
+ import { type RedisClient } from '../redis.js';
3
3
  export declare class RedisTokenStorage extends TokenStorage {
4
4
  redis: RedisClient;
5
5
  keyPrefix: string;
@@ -1,6 +1,6 @@
1
1
  import { config } from '@devmoods/config';
2
- import {} from '../redis.js';
3
2
  import { TokenStorage } from './types.js';
3
+ import {} from '../redis.js';
4
4
  export class RedisTokenStorage extends TokenStorage {
5
5
  redis;
6
6
  keyPrefix;
@@ -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<string>;
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<string>;
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(`Invalid token: ${error.message}`, { cause: error }));
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
- await tokenStorage.set(otp, String(user.id), config.AUTH_OTP_EXPIRES);
206
- return otp;
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 storedUid = await tokenStorage.get(otp);
210
- if (!storedUid) {
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
- /* c8 ignore next 3 */
214
- if (user && storedUid !== user.id) {
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 storedUid;
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 interface CacheAdapter {
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 function createMemoryCacheAdapter(): CacheAdapter;
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: (...args: Arguments) => Promise<void>;
45
+ clear(...args: Arguments): Promise<void>;
25
46
  };
26
47
  hashKey: typeof hashKey;
27
48
  };