ideal-auth 1.3.2 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -163,6 +163,8 @@ const session = auth();
163
163
  await session.attempt({ email, password }); // password verified internally
164
164
  ```
165
165
 
166
+ > **Timing note:** `attempt()` runs `hash.verify()` against a cached dummy hash when the user is not found, so the user-exists and user-missing paths take roughly the same time. For full protection against user enumeration, ensure your `resolveUserByCredentials` lookup is not catastrophically slower for non-existent users (e.g. use an indexed column).
167
+
166
168
  **Manual (escape hatch):** Provide `attemptUser` for full control over lookup and verification. Takes precedence over the Laravel-style config if both are provided.
167
169
 
168
170
  ```typescript
@@ -1,5 +1,17 @@
1
1
  import { seal, unseal } from './session/seal';
2
2
  import { buildCookieOptions } from './session/cookie';
3
+ // Equalize attempt() timing between user-found and user-missing paths by always
4
+ // running hash.verify(). The dummy hash is cached per HashInstance so repeated
5
+ // misses match the cost of a real verify.
6
+ const dummyHashCache = new WeakMap();
7
+ function getDummyHash(hash) {
8
+ let p = dummyHashCache.get(hash);
9
+ if (!p) {
10
+ p = hash.make('__ideal-auth-dummy__');
11
+ dummyHashCache.set(hash, p);
12
+ }
13
+ return p;
14
+ }
3
15
  export function createAuthInstance(deps) {
4
16
  let cachedPayload;
5
17
  let cachedUser;
@@ -112,12 +124,14 @@ export function createAuthInstance(deps) {
112
124
  if (deps.hash && deps.resolveUserByCredentials) {
113
125
  const { [deps.credentialKey]: password, ...lookup } = credentials;
114
126
  const dbUser = await deps.resolveUserByCredentials(lookup);
115
- if (!dbUser)
116
- return false;
117
- const storedHash = dbUser[deps.passwordField];
118
- if (!storedHash || !(await deps.hash.verify(password, storedHash))) {
127
+ // Run verify even on miss against a dummy hash — prevents user enumeration via timing
128
+ const storedHash = dbUser
129
+ ? dbUser[deps.passwordField]
130
+ : undefined;
131
+ const hashToCheck = storedHash ?? (await getDummyHash(deps.hash));
132
+ const ok = await deps.hash.verify(password, hashToCheck);
133
+ if (!dbUser || !storedHash || !ok)
119
134
  return false;
120
- }
121
135
  await writeSession(dbUser, options);
122
136
  return true;
123
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ideal-auth",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "Auth primitives for the JS ecosystem. Zero framework dependencies.",
5
5
  "scripts": {
6
6
  "build": "tsc",