ideal-auth 0.4.0 → 0.5.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/README.md +11 -8
- package/dist/auth-instance.d.ts +3 -3
- package/dist/auth-instance.js +1 -1
- package/dist/types.d.ts +32 -25
- package/package.json +1 -1
- package/skills/ideal-auth/SKILL.md +48 -1
package/README.md
CHANGED
|
@@ -12,17 +12,19 @@ Provide a cookie bridge (3 functions) once during setup, and `auth().login(user)
|
|
|
12
12
|
bun add ideal-auth
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
## Generate
|
|
15
|
+
## Generate Secrets
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
+
# Session secret (required — used by createAuth)
|
|
18
19
|
bunx ideal-auth secret
|
|
19
|
-
|
|
20
|
+
# IDEAL_AUTH_SECRET=aLThikMgJKMBB5WZLE-lCaOQUdgPWU8BHRv99bkYaVY
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
# Encryption key (optional — used by encrypt/decrypt for data at rest)
|
|
23
|
+
bunx ideal-auth encryption-key
|
|
24
|
+
# ENCRYPTION_KEY=9546dd9fa461ce15f0aacd6e1b461b52
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
Copy the output into your `.env` file.
|
|
27
|
+
Copy the output into your `.env` file. `IDEAL_AUTH_SECRET` must be at least 32 characters. `ENCRYPTION_KEY` is only needed if you use `encrypt()`/`decrypt()` (e.g., encrypting TOTP secrets or access tokens at rest).
|
|
26
28
|
|
|
27
29
|
## Quick Start
|
|
28
30
|
|
|
@@ -94,12 +96,13 @@ Returns a function `auth()` that creates an `AuthInstance` on each call.
|
|
|
94
96
|
| --- | --- | --- | --- |
|
|
95
97
|
| `secret` | `string` | Yes | — |
|
|
96
98
|
| `cookie` | `CookieBridge` | Yes | — |
|
|
97
|
-
| `resolveUser` | `(id: string) => Promise<User \| null>` | Yes | — |
|
|
99
|
+
| `resolveUser` | `(id: string) => Promise<User \| null \| undefined>` | Yes (unless `sessionFields` is provided) | — |
|
|
100
|
+
| `sessionFields` | `(keyof User & string)[]` | Yes (unless `resolveUser` is provided) | — |
|
|
98
101
|
| `hash` | `HashInstance` | No | — |
|
|
99
|
-
| `resolveUserByCredentials` | `(creds: Record<string, any>) => Promise<User \| null>` | No | — |
|
|
102
|
+
| `resolveUserByCredentials` | `(creds: Record<string, any>) => Promise<User \| null \| undefined>` | No | — |
|
|
100
103
|
| `credentialKey` | `string` | No | `'password'` |
|
|
101
104
|
| `passwordField` | `string` | No | `'password'` |
|
|
102
|
-
| `attemptUser` | `(creds: Record<string, any>) => Promise<User \| null>` | No | — |
|
|
105
|
+
| `attemptUser` | `(creds: Record<string, any>) => Promise<User \| null \| undefined>` | No | — |
|
|
103
106
|
| `session.cookieName` | `string` | No | `'ideal_session'` |
|
|
104
107
|
| `session.maxAge` | `number` (seconds) | No | `604800` (7 days) |
|
|
105
108
|
| `session.rememberMaxAge` | `number` (seconds) | No | `2592000` (30 days) |
|
package/dist/auth-instance.d.ts
CHANGED
|
@@ -6,13 +6,13 @@ interface AuthInstanceDeps<TUser extends AnyUser> {
|
|
|
6
6
|
maxAge: number;
|
|
7
7
|
rememberMaxAge: number;
|
|
8
8
|
cookieOptions: ConfigurableCookieOptions;
|
|
9
|
-
resolveUser?: (id: string) => Promise<TUser | null>;
|
|
9
|
+
resolveUser?: (id: string) => Promise<TUser | null | undefined>;
|
|
10
10
|
sessionFields?: (keyof TUser & string)[];
|
|
11
11
|
hash?: HashInstance;
|
|
12
|
-
resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<TUser | null>;
|
|
12
|
+
resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
|
|
13
13
|
credentialKey: string;
|
|
14
14
|
passwordField: string;
|
|
15
|
-
attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null>;
|
|
15
|
+
attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
|
|
16
16
|
}
|
|
17
17
|
export declare function createAuthInstance<TUser extends AnyUser>(deps: AuthInstanceDeps<TUser>): AuthInstance<TUser>;
|
|
18
18
|
export {};
|
package/dist/auth-instance.js
CHANGED
|
@@ -106,7 +106,7 @@ export function createAuthInstance(deps) {
|
|
|
106
106
|
}
|
|
107
107
|
// Database-backed: resolve user via callback
|
|
108
108
|
if (deps.resolveUser) {
|
|
109
|
-
cachedUser = await deps.resolveUser(session.uid);
|
|
109
|
+
cachedUser = (await deps.resolveUser(session.uid)) ?? null;
|
|
110
110
|
return cachedUser;
|
|
111
111
|
}
|
|
112
112
|
cachedUser = null;
|
package/dist/types.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface SessionPayload {
|
|
|
27
27
|
export interface LoginOptions {
|
|
28
28
|
remember?: boolean;
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
interface AuthConfigBase<TUser extends AnyUser> {
|
|
31
31
|
secret: string;
|
|
32
32
|
cookie: CookieBridge;
|
|
33
33
|
session?: {
|
|
@@ -36,33 +36,39 @@ export interface AuthConfig<TUser extends AnyUser = AnyUser> {
|
|
|
36
36
|
rememberMaxAge?: number;
|
|
37
37
|
cookie?: Partial<ConfigurableCookieOptions>;
|
|
38
38
|
};
|
|
39
|
-
resolveUser?: (id: string) => Promise<TUser | null>;
|
|
40
|
-
/**
|
|
41
|
-
* Fields from the user object to store in the session cookie.
|
|
42
|
-
* When provided, `resolveUser` is not needed — `user()` returns
|
|
43
|
-
* the stored fields directly from the cookie.
|
|
44
|
-
*
|
|
45
|
-
* The `id` field is always stored. List only additional fields.
|
|
46
|
-
* Keep the total small — session cookies have a ~4KB size limit.
|
|
47
|
-
*
|
|
48
|
-
* Cannot be used together with `resolveUser`.
|
|
49
|
-
*
|
|
50
|
-
* **Staleness:** Data is snapshotted at login time. If a user's role
|
|
51
|
-
* or permissions change server-side, the cookie retains the old values
|
|
52
|
-
* until the user re-logs in. For authorization-critical fields (role,
|
|
53
|
-
* permissions, subscription tier), prefer `resolveUser` to get fresh
|
|
54
|
-
* data on every request.
|
|
55
|
-
*
|
|
56
|
-
* **ID type:** `user()` always returns `id` as a `string` on subsequent
|
|
57
|
-
* requests (read from cookie), even if the original `TUser.id` was a number.
|
|
58
|
-
*/
|
|
59
|
-
sessionFields?: (keyof TUser & string)[];
|
|
60
39
|
hash?: HashInstance;
|
|
61
|
-
resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<TUser | null>;
|
|
40
|
+
resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
|
|
62
41
|
credentialKey?: string;
|
|
63
42
|
passwordField?: string;
|
|
64
|
-
attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null>;
|
|
65
|
-
}
|
|
43
|
+
attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
|
|
44
|
+
}
|
|
45
|
+
/** Database-backed: `user()` calls `resolveUser(id)` on every request. */
|
|
46
|
+
interface AuthConfigWithResolveUser<TUser extends AnyUser> extends AuthConfigBase<TUser> {
|
|
47
|
+
resolveUser: (id: string) => Promise<TUser | null | undefined>;
|
|
48
|
+
/** Cannot use `sessionFields` together with `resolveUser`. */
|
|
49
|
+
sessionFields?: never;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Cookie-backed: `user()` reads declared fields from the session cookie.
|
|
53
|
+
*
|
|
54
|
+
* The `id` field is always stored. List only additional fields.
|
|
55
|
+
* Keep the total small — session cookies have a ~4KB size limit.
|
|
56
|
+
*
|
|
57
|
+
* **Staleness:** Data is snapshotted at login time. If a user's role
|
|
58
|
+
* or permissions change server-side, the cookie retains the old values
|
|
59
|
+
* until the user re-logs in. For authorization-critical fields (role,
|
|
60
|
+
* permissions, subscription tier), prefer `resolveUser` to get fresh
|
|
61
|
+
* data on every request.
|
|
62
|
+
*
|
|
63
|
+
* **ID type:** `user()` always returns `id` as a `string` on subsequent
|
|
64
|
+
* requests (read from cookie), even if the original `TUser.id` was a number.
|
|
65
|
+
*/
|
|
66
|
+
interface AuthConfigWithSessionFields<TUser extends AnyUser> extends AuthConfigBase<TUser> {
|
|
67
|
+
/** Cannot use `resolveUser` together with `sessionFields`. */
|
|
68
|
+
resolveUser?: never;
|
|
69
|
+
sessionFields: (keyof TUser & string)[];
|
|
70
|
+
}
|
|
71
|
+
export type AuthConfig<TUser extends AnyUser = AnyUser> = AuthConfigWithResolveUser<TUser> | AuthConfigWithSessionFields<TUser>;
|
|
66
72
|
export interface HashConfig {
|
|
67
73
|
rounds?: number;
|
|
68
74
|
}
|
|
@@ -125,3 +131,4 @@ export interface RecoveryCodeResult {
|
|
|
125
131
|
valid: boolean;
|
|
126
132
|
remaining: string[];
|
|
127
133
|
}
|
|
134
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ideal-auth
|
|
3
|
-
description: Auth primitives for the JS ecosystem. Covers login, logout, session, register, signup, sign in, sign out, password, 2FA, two-factor, TOTP, MFA, ideal-auth, cookie bridge, middleware, route protection, rate limit, rate limiting, password reset, forgot password, email verification, remember me, recovery code, CSRF, encrypt, decrypt, hash, bcrypt, token, secret, session cookie, multi-tenant, cross-domain, tenant, transfer token, federated logout, OAuth redirect, central login, login session, attemptUser, sessionFields, cookie-backed session, resolveUser, token refresh, refresh token, access token, OIDC.
|
|
3
|
+
description: Auth primitives for the JS ecosystem. Covers login, logout, session, register, signup, sign in, sign out, password, 2FA, two-factor, TOTP, MFA, ideal-auth, cookie bridge, middleware, route protection, rate limit, rate limiting, password reset, forgot password, email verification, remember me, recovery code, CSRF, encrypt, decrypt, hash, bcrypt, token, secret, session cookie, multi-tenant, cross-domain, tenant, transfer token, federated logout, OAuth redirect, central login, login session, attemptUser, sessionFields, cookie-backed session, resolveUser, token refresh, refresh token, access token, OIDC, passkey, passkeys, WebAuthn, passwordless, biometric, fingerprint.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
You are an expert on `ideal-auth`, the auth primitives library for the JS ecosystem. You have complete knowledge of its API, patterns, security model, and framework integrations. Use this knowledge to help users implement authentication correctly.
|
|
@@ -1622,6 +1622,53 @@ await auth().login(user, { remember: true });
|
|
|
1622
1622
|
|
|
1623
1623
|
---
|
|
1624
1624
|
|
|
1625
|
+
## Passkeys (WebAuthn)
|
|
1626
|
+
|
|
1627
|
+
Passkeys use public-key cryptography for passwordless authentication. ideal-auth handles the session after verification — the WebAuthn protocol is handled by `@simplewebauthn/server` and `@simplewebauthn/browser`.
|
|
1628
|
+
|
|
1629
|
+
No `hash` or `bcryptjs` needed — there are no passwords.
|
|
1630
|
+
|
|
1631
|
+
### Flow
|
|
1632
|
+
|
|
1633
|
+
```
|
|
1634
|
+
Registration: browser creates key pair → store public key in DB
|
|
1635
|
+
Authentication: server sends challenge → browser signs it → server verifies → auth().login(user)
|
|
1636
|
+
```
|
|
1637
|
+
|
|
1638
|
+
### After WebAuthn verification, create session with ideal-auth
|
|
1639
|
+
|
|
1640
|
+
```ts
|
|
1641
|
+
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
|
|
1642
|
+
import { auth } from './auth';
|
|
1643
|
+
|
|
1644
|
+
const verification = await verifyAuthenticationResponse({
|
|
1645
|
+
response: body,
|
|
1646
|
+
expectedChallenge,
|
|
1647
|
+
expectedOrigin: origin,
|
|
1648
|
+
expectedRPID: rpID,
|
|
1649
|
+
credential: { id: passkey.id, publicKey: Buffer.from(passkey.publicKey, 'base64url'), counter: passkey.counter },
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
if (verification.verified) {
|
|
1653
|
+
// Update signature counter (replay protection)
|
|
1654
|
+
await db.passkey.update({ where: { id: passkey.id }, data: { counter: Number(verification.authenticationInfo.newCounter) } });
|
|
1655
|
+
|
|
1656
|
+
const user = await db.user.findUnique({ where: { id: passkey.userId } });
|
|
1657
|
+
await auth().login(user); // session created — done
|
|
1658
|
+
}
|
|
1659
|
+
```
|
|
1660
|
+
|
|
1661
|
+
### Key points
|
|
1662
|
+
- Install: `bun add @simplewebauthn/server @simplewebauthn/browser`
|
|
1663
|
+
- Store challenges in httpOnly cookies (5-min expiry), not client-side
|
|
1664
|
+
- Always update the signature counter after authentication
|
|
1665
|
+
- Set `rpID` to root domain (e.g., `example.com`) — works across subdomains
|
|
1666
|
+
- `userVerification: 'preferred'` for most apps, `'required'` for high-security
|
|
1667
|
+
- Passkeys with user verification can replace 2FA (biometric/PIN is the second factor)
|
|
1668
|
+
- Rate limit the options and verify endpoints
|
|
1669
|
+
|
|
1670
|
+
---
|
|
1671
|
+
|
|
1625
1672
|
## Open Redirect Prevention
|
|
1626
1673
|
|
|
1627
1674
|
Always validate redirect URLs after login. Never redirect to user-supplied absolute URLs.
|