ideal-auth 1.3.1 → 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;
@@ -10,7 +22,11 @@ export function createAuthInstance(deps) {
10
22
  }
11
23
  }
12
24
  async function readSession() {
13
- validateSecret();
25
+ // Fail closed on reads — no secret means no session, not an error
26
+ if (!deps.secret || deps.secret.length < 32) {
27
+ cachedPayload = null;
28
+ return null;
29
+ }
14
30
  if (cachedPayload !== undefined)
15
31
  return cachedPayload;
16
32
  const raw = await deps.cookie.get(deps.cookieName);
@@ -108,12 +124,14 @@ export function createAuthInstance(deps) {
108
124
  if (deps.hash && deps.resolveUserByCredentials) {
109
125
  const { [deps.credentialKey]: password, ...lookup } = credentials;
110
126
  const dbUser = await deps.resolveUserByCredentials(lookup);
111
- if (!dbUser)
112
- return false;
113
- const storedHash = dbUser[deps.passwordField];
114
- 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)
115
134
  return false;
116
- }
117
135
  await writeSession(dbUser, options);
118
136
  return true;
119
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ideal-auth",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Auth primitives for the JS ecosystem. Zero framework dependencies.",
5
5
  "scripts": {
6
6
  "build": "tsc",