@revealui/auth 0.3.6 → 0.3.8
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 +14 -14
- package/dist/react/useMFA.d.ts +1 -1
- package/dist/react/useMFA.d.ts.map +1 -1
- package/dist/server/audit-bridge.d.ts +1 -1
- package/dist/server/audit-bridge.js +2 -2
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +15 -4
- package/dist/server/mfa-enforcement.js +1 -1
- package/dist/server/mfa.d.ts +1 -1
- package/dist/server/mfa.js +2 -2
- package/dist/server/oauth.d.ts +6 -4
- package/dist/server/oauth.d.ts.map +1 -1
- package/dist/server/oauth.js +18 -11
- package/dist/server/passkey.d.ts +3 -3
- package/dist/server/passkey.d.ts.map +1 -1
- package/dist/server/passkey.js +1 -1
- package/dist/server/password-reset.d.ts.map +1 -1
- package/dist/server/password-reset.js +11 -3
- package/dist/server/password-validation.d.ts +11 -0
- package/dist/server/password-validation.d.ts.map +1 -1
- package/dist/server/password-validation.js +37 -0
- package/dist/server/providers/github.d.ts +3 -3
- package/dist/server/providers/github.d.ts.map +1 -1
- package/dist/server/providers/github.js +20 -12
- package/dist/server/providers/google.d.ts +3 -3
- package/dist/server/providers/google.d.ts.map +1 -1
- package/dist/server/providers/google.js +21 -13
- package/dist/server/providers/vercel.d.ts +4 -4
- package/dist/server/providers/vercel.d.ts.map +1 -1
- package/dist/server/providers/vercel.js +7 -7
- package/dist/server/session.d.ts +2 -2
- package/dist/server/session.d.ts.map +1 -1
- package/dist/server/session.js +2 -2
- package/dist/server/signed-cookie.d.ts.map +1 -1
- package/dist/server/signed-cookie.js +4 -2
- package/dist/server/storage/index.js +2 -2
- package/package.json +22 -12
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# @revealui/auth
|
|
2
2
|
|
|
3
|
-
Session-based authentication for RevealUI
|
|
3
|
+
Session-based authentication for RevealUI - database-backed sessions, rate limiting, brute force protection, and password reset.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Database Sessions**
|
|
8
|
-
- **Secure Cookies**
|
|
9
|
-
- **Rate Limiting**
|
|
10
|
-
- **Brute Force Protection**
|
|
11
|
-
- **Password Reset**
|
|
12
|
-
- **Password Validation**
|
|
13
|
-
- **React Hooks**
|
|
14
|
-
- **Framework Agnostic**
|
|
7
|
+
- **Database Sessions** - PostgreSQL/NeonDB-backed sessions with SHA-256 token hashing
|
|
8
|
+
- **Secure Cookies** - HTTP-only, SameSite, secure flag, cross-subdomain support
|
|
9
|
+
- **Rate Limiting** - Configurable per-endpoint rate limits stored in database
|
|
10
|
+
- **Brute Force Protection** - Progressive lockout on failed sign-in attempts
|
|
11
|
+
- **Password Reset** - Token-based password reset flow with email integration
|
|
12
|
+
- **Password Validation** - Strength requirements and common password checks
|
|
13
|
+
- **React Hooks** - Client-side session management (`useSession`, `useSignIn`, `useSignOut`)
|
|
14
|
+
- **Framework Agnostic** - Works with Next.js, Hono, and other Node.js frameworks
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
@@ -95,8 +95,8 @@ pnpm test
|
|
|
95
95
|
- You need session-based auth with database-backed sessions for a RevealUI app
|
|
96
96
|
- You want built-in brute force protection and rate limiting without external services
|
|
97
97
|
- You need React hooks for client-side session management (`useSession`, `useSignIn`, `useSignOut`)
|
|
98
|
-
- **Not** for OAuth-only flows
|
|
99
|
-
- **Not** for stateless JWT auth
|
|
98
|
+
- **Not** for OAuth-only flows - use a dedicated OAuth provider and wire tokens through this package
|
|
99
|
+
- **Not** for stateless JWT auth - this package uses database sessions by design
|
|
100
100
|
|
|
101
101
|
## JOSHUA Alignment
|
|
102
102
|
|
|
@@ -106,9 +106,9 @@ pnpm test
|
|
|
106
106
|
|
|
107
107
|
## Related
|
|
108
108
|
|
|
109
|
-
- [Core Package](../core/README.md)
|
|
110
|
-
- [DB Package](../db/README.md)
|
|
111
|
-
- [Auth Guide](../../docs/AUTH.md)
|
|
109
|
+
- [Core Package](../core/README.md) - Runtime engine (uses auth for access control)
|
|
110
|
+
- [DB Package](../db/README.md) - Database schema (sessions, users, rate_limits tables)
|
|
111
|
+
- [Auth Guide](../../docs/AUTH.md) - Architecture, usage patterns, and security design
|
|
112
112
|
|
|
113
113
|
## License
|
|
114
114
|
|
package/dist/react/useMFA.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface MFASetupData {
|
|
|
15
15
|
backupCodes: string[];
|
|
16
16
|
}
|
|
17
17
|
export interface UseMFASetupResult {
|
|
18
|
-
/** Initiate MFA setup
|
|
18
|
+
/** Initiate MFA setup - returns secret, QR URI, and backup codes */
|
|
19
19
|
setup: () => Promise<MFASetupData | null>;
|
|
20
20
|
/** Verify a TOTP code to confirm setup */
|
|
21
21
|
verifySetup: (code: string) => Promise<boolean>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useMFA.d.ts","sourceRoot":"","sources":["../../src/react/useMFA.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,iDAAiD;IACjD,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,
|
|
1
|
+
{"version":3,"file":"useMFA.d.ts","sourceRoot":"","sources":["../../src/react/useMFA.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,iDAAiD;IACjD,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,KAAK,EAAE,MAAM,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC1C,0CAA0C;IAC1C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAChD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,IAAI,iBAAiB,CAqE/C;AAED,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,wCAAwC;IACxC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,IAAI,kBAAkB,CAsEjD"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Security Event Bridge
|
|
2
|
+
* Security Event Bridge - Connects auth events to the audit trail.
|
|
3
3
|
*
|
|
4
4
|
* Each function wraps an auth operation with structured audit logging
|
|
5
5
|
* via the AuditSystem from @revealui/security. Uses lazy import to
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Security Event Bridge
|
|
2
|
+
* Security Event Bridge - Connects auth events to the audit trail.
|
|
3
3
|
*
|
|
4
4
|
* Each function wraps an auth operation with structured audit logging
|
|
5
5
|
* via the AuditSystem from @revealui/security. Uses lazy import to
|
|
@@ -20,7 +20,7 @@ async function getAudit() {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Internal helper
|
|
23
|
+
* Internal helper - logs an audit event, silently skipping if the
|
|
24
24
|
* audit system is unavailable.
|
|
25
25
|
*/
|
|
26
26
|
async function logAuditEvent(event) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAQ,MAAM,aAAa,CAAC;AASpE;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAQ,MAAM,aAAa,CAAC;AASpE;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC,YAAY,CAAC,CAuKvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAiBtD;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,YAAY,CAAC,CAgMvB"}
|
package/dist/server/auth.js
CHANGED
|
@@ -12,7 +12,7 @@ import { and, eq, isNull } from 'drizzle-orm';
|
|
|
12
12
|
import { clearFailedAttempts, isAccountLocked, recordFailedAttempt } from './brute-force.js';
|
|
13
13
|
import { validatePasswordStrength } from './password-validation.js';
|
|
14
14
|
import { checkRateLimit } from './rate-limit.js';
|
|
15
|
-
import { createSession } from './session.js';
|
|
15
|
+
import { createSession, rotateSession } from './session.js';
|
|
16
16
|
/** Grace period after signup during which unverified users can still sign in (24 hours) */
|
|
17
17
|
const EMAIL_VERIFICATION_GRACE_PERIOD_MS = 24 * 60 * 60 * 1000;
|
|
18
18
|
/**
|
|
@@ -137,7 +137,7 @@ export async function signIn(email, password, options) {
|
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
-
// Check if MFA is enabled
|
|
140
|
+
// Check if MFA is enabled - if so, return early and require TOTP verification
|
|
141
141
|
if (user.mfaEnabled) {
|
|
142
142
|
return {
|
|
143
143
|
success: true,
|
|
@@ -145,10 +145,12 @@ export async function signIn(email, password, options) {
|
|
|
145
145
|
mfaUserId: user.id,
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
|
-
//
|
|
148
|
+
// Rotate session: delete all existing sessions for this user, then create
|
|
149
|
+
// a fresh one. This prevents session fixation attacks where an attacker
|
|
150
|
+
// plants a session token that the victim later authenticates.
|
|
149
151
|
let token;
|
|
150
152
|
try {
|
|
151
|
-
const sessionResult = await
|
|
153
|
+
const sessionResult = await rotateSession(user.id, {
|
|
152
154
|
userAgent: options?.userAgent || 'Unknown',
|
|
153
155
|
ipAddress: options?.ipAddress || 'Unknown',
|
|
154
156
|
});
|
|
@@ -238,6 +240,15 @@ export async function signUp(email, password, name, options) {
|
|
|
238
240
|
error: passwordValidation.errors.join('. '),
|
|
239
241
|
};
|
|
240
242
|
}
|
|
243
|
+
// Check password against known data breaches (non-blocking on failure)
|
|
244
|
+
const { checkPasswordBreach } = await import('./password-validation.js');
|
|
245
|
+
const breachCount = await checkPasswordBreach(password);
|
|
246
|
+
if (breachCount > 0) {
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
error: `This password has appeared in ${breachCount.toLocaleString()} data breaches. Please choose a different password.`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
241
252
|
let db;
|
|
242
253
|
try {
|
|
243
254
|
db = getClient();
|
|
@@ -39,7 +39,7 @@ export function requireMfa(options = {}) {
|
|
|
39
39
|
return (request) => {
|
|
40
40
|
const session = request.session;
|
|
41
41
|
if (!session) {
|
|
42
|
-
// No session
|
|
42
|
+
// No session - nothing to enforce (auth middleware handles this)
|
|
43
43
|
return { allowed: true };
|
|
44
44
|
}
|
|
45
45
|
const user = session.user;
|
package/dist/server/mfa.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MFA/2FA
|
|
2
|
+
* MFA/2FA - TOTP-based Multi-Factor Authentication
|
|
3
3
|
*
|
|
4
4
|
* Uses the timing-safe TOTP implementation from @revealui/core/security/auth.
|
|
5
5
|
* Backup codes are bcrypt-hashed for storage (one-time use, consumed on verify).
|
package/dist/server/mfa.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MFA/2FA
|
|
2
|
+
* MFA/2FA - TOTP-based Multi-Factor Authentication
|
|
3
3
|
*
|
|
4
4
|
* Uses the timing-safe TOTP implementation from @revealui/core/security/auth.
|
|
5
5
|
* Backup codes are bcrypt-hashed for storage (one-time use, consumed on verify).
|
|
@@ -274,7 +274,7 @@ export async function disableMFA(userId, proof) {
|
|
|
274
274
|
return { success: false, error: 'Invalid password' };
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
|
-
// For passkey proof, the API route has already performed the WebAuthn assertion
|
|
277
|
+
// For passkey proof, the API route has already performed the WebAuthn assertion -
|
|
278
278
|
// the `verified: true` flag is trusted as a server-side signal.
|
|
279
279
|
// Clear all MFA data
|
|
280
280
|
await db
|
package/dist/server/oauth.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OAuth Core
|
|
2
|
+
* OAuth Core - State Management + User Upsert
|
|
3
3
|
*
|
|
4
4
|
* CSRF state: signed cookie using HMAC-SHA256 over a base64url payload.
|
|
5
5
|
* Provider dispatch: routes to Google / GitHub / Vercel provider modules.
|
|
@@ -16,7 +16,7 @@ export interface ProviderUser {
|
|
|
16
16
|
* Generate a signed OAuth state token.
|
|
17
17
|
*
|
|
18
18
|
* State encodes provider + redirectTo + nonce as base64url JSON.
|
|
19
|
-
* Cookie value is `<state>.<hmac>`
|
|
19
|
+
* Cookie value is `<state>.<hmac>` - the HMAC is over the state string
|
|
20
20
|
* using REVEALUI_SECRET, providing CSRF protection without a DB table.
|
|
21
21
|
*/
|
|
22
22
|
export declare function generateOAuthState(provider: string, redirectTo: string, options?: {
|
|
@@ -24,6 +24,7 @@ export declare function generateOAuthState(provider: string, redirectTo: string,
|
|
|
24
24
|
}): {
|
|
25
25
|
state: string;
|
|
26
26
|
cookieValue: string;
|
|
27
|
+
codeChallenge: string;
|
|
27
28
|
};
|
|
28
29
|
/**
|
|
29
30
|
* Verify a signed OAuth state token from the callback.
|
|
@@ -34,9 +35,10 @@ export declare function verifyOAuthState(state: string | null | undefined, cooki
|
|
|
34
35
|
provider: string;
|
|
35
36
|
redirectTo: string;
|
|
36
37
|
linkConsent?: boolean;
|
|
38
|
+
codeVerifier?: string;
|
|
37
39
|
} | null;
|
|
38
|
-
export declare function buildAuthUrl(provider: string, redirectUri: string, state: string): string;
|
|
39
|
-
export declare function exchangeCode(provider: string, code: string, redirectUri: string): Promise<string>;
|
|
40
|
+
export declare function buildAuthUrl(provider: string, redirectUri: string, state: string, codeChallenge?: string): string;
|
|
41
|
+
export declare function exchangeCode(provider: string, code: string, redirectUri: string, codeVerifier?: string): Promise<string>;
|
|
40
42
|
export declare function fetchProviderUser(provider: string, accessToken: string): Promise<ProviderUser>;
|
|
41
43
|
export interface UpsertOAuthOptions {
|
|
42
44
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/server/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAMxC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAMD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAClC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/server/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAMxC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAMD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAClC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAuB/D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAwD/F;AAwBD,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,CASR;AAED,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CAQvB;AAMD,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,YAAY,EAC1B,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAmGf;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,IAAI,CAAC,CAiEf;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCxF;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAAC,CAajG"}
|
package/dist/server/oauth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OAuth Core
|
|
2
|
+
* OAuth Core - State Management + User Upsert
|
|
3
3
|
*
|
|
4
4
|
* CSRF state: signed cookie using HMAC-SHA256 over a base64url payload.
|
|
5
5
|
* Provider dispatch: routes to Google / GitHub / Vercel provider modules.
|
|
@@ -21,15 +21,19 @@ import * as vercel from './providers/vercel.js';
|
|
|
21
21
|
* Generate a signed OAuth state token.
|
|
22
22
|
*
|
|
23
23
|
* State encodes provider + redirectTo + nonce as base64url JSON.
|
|
24
|
-
* Cookie value is `<state>.<hmac>`
|
|
24
|
+
* Cookie value is `<state>.<hmac>` - the HMAC is over the state string
|
|
25
25
|
* using REVEALUI_SECRET, providing CSRF protection without a DB table.
|
|
26
26
|
*/
|
|
27
27
|
export function generateOAuthState(provider, redirectTo, options) {
|
|
28
28
|
const nonce = crypto.randomBytes(16).toString('hex');
|
|
29
|
+
// PKCE: generate a code_verifier (RFC 7636 §4.1 - 32 random bytes → 43 base64url chars)
|
|
30
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
31
|
+
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
29
32
|
const payload = JSON.stringify({
|
|
30
33
|
provider,
|
|
31
34
|
redirectTo,
|
|
32
35
|
nonce,
|
|
36
|
+
cv: codeVerifier,
|
|
33
37
|
...(options?.linkConsent ? { linkConsent: true } : {}),
|
|
34
38
|
});
|
|
35
39
|
const state = Buffer.from(payload).toString('base64url');
|
|
@@ -38,8 +42,9 @@ export function generateOAuthState(provider, redirectTo, options) {
|
|
|
38
42
|
throw new Error('REVEALUI_SECRET is required for OAuth state signing. ' +
|
|
39
43
|
'Set it in your environment variables.');
|
|
40
44
|
}
|
|
45
|
+
// lgtm[js/insufficient-password-hash] - HMAC-SHA256 for CSRF state signing, not password hashing
|
|
41
46
|
const hmac = crypto.createHmac('sha256', secret).update(state).digest('hex');
|
|
42
|
-
return { state, cookieValue: `${state}.${hmac}
|
|
47
|
+
return { state, cookieValue: `${state}.${hmac}`, codeChallenge };
|
|
43
48
|
}
|
|
44
49
|
/**
|
|
45
50
|
* Verify a signed OAuth state token from the callback.
|
|
@@ -64,8 +69,9 @@ export function verifyOAuthState(state, cookieValue) {
|
|
|
64
69
|
throw new Error('REVEALUI_SECRET is required for OAuth state verification. ' +
|
|
65
70
|
'Set it in your environment variables.');
|
|
66
71
|
}
|
|
72
|
+
// lgtm[js/insufficient-password-hash] - HMAC-SHA256 for CSRF state verification, not password hashing
|
|
67
73
|
const expectedHmac = crypto.createHmac('sha256', secret).update(state).digest('hex');
|
|
68
|
-
// Both are hex-encoded SHA-256 HMACs
|
|
74
|
+
// Both are hex-encoded SHA-256 HMACs - must be exactly 64 hex characters.
|
|
69
75
|
// Reject wrong-length inputs immediately; do NOT pad (padding enables forged matches
|
|
70
76
|
// where a short storedHmac is zero-padded to collide with the expected hash).
|
|
71
77
|
if (storedHmac.length !== 64 || expectedHmac.length !== 64)
|
|
@@ -84,6 +90,7 @@ export function verifyOAuthState(state, cookieValue) {
|
|
|
84
90
|
provider: parsed.provider,
|
|
85
91
|
redirectTo: parsed.redirectTo,
|
|
86
92
|
...(parsed.linkConsent ? { linkConsent: true } : {}),
|
|
93
|
+
...(parsed.cv ? { codeVerifier: parsed.cv } : {}),
|
|
87
94
|
};
|
|
88
95
|
}
|
|
89
96
|
catch {
|
|
@@ -108,7 +115,7 @@ function getClientId(provider) {
|
|
|
108
115
|
throw new Error(`Missing client ID for provider: ${provider}`);
|
|
109
116
|
return id;
|
|
110
117
|
}
|
|
111
|
-
export function buildAuthUrl(provider, redirectUri, state) {
|
|
118
|
+
export function buildAuthUrl(provider, redirectUri, state, codeChallenge) {
|
|
112
119
|
if (!isProvider(provider))
|
|
113
120
|
throw new Error(`Unknown provider: ${provider}`);
|
|
114
121
|
const clientId = getClientId(provider);
|
|
@@ -117,9 +124,9 @@ export function buildAuthUrl(provider, redirectUri, state) {
|
|
|
117
124
|
github: github.buildAuthUrl,
|
|
118
125
|
vercel: vercel.buildAuthUrl,
|
|
119
126
|
};
|
|
120
|
-
return builders[provider](clientId, redirectUri, state);
|
|
127
|
+
return builders[provider](clientId, redirectUri, state, codeChallenge);
|
|
121
128
|
}
|
|
122
|
-
export async function exchangeCode(provider, code, redirectUri) {
|
|
129
|
+
export async function exchangeCode(provider, code, redirectUri, codeVerifier) {
|
|
123
130
|
if (!isProvider(provider))
|
|
124
131
|
throw new Error(`Unknown provider: ${provider}`);
|
|
125
132
|
const exchangers = {
|
|
@@ -127,7 +134,7 @@ export async function exchangeCode(provider, code, redirectUri) {
|
|
|
127
134
|
github: github.exchangeCode,
|
|
128
135
|
vercel: vercel.exchangeCode,
|
|
129
136
|
};
|
|
130
|
-
return exchangers[provider](code, redirectUri);
|
|
137
|
+
return exchangers[provider](code, redirectUri, codeVerifier);
|
|
131
138
|
}
|
|
132
139
|
export async function fetchProviderUser(provider, accessToken) {
|
|
133
140
|
if (!isProvider(provider))
|
|
@@ -194,7 +201,7 @@ export async function upsertOAuthUser(provider, providerUser, options) {
|
|
|
194
201
|
.limit(1);
|
|
195
202
|
if (existingUser) {
|
|
196
203
|
if (options?.linkConsent) {
|
|
197
|
-
// User explicitly consented to link
|
|
204
|
+
// User explicitly consented to link - use the existing account
|
|
198
205
|
userId = existingUser.id;
|
|
199
206
|
isNewUser = false;
|
|
200
207
|
logger.info(`Linking ${provider} account to existing user ${userId} (consent-based)`);
|
|
@@ -266,7 +273,7 @@ export async function linkOAuthAccount(userId, provider, providerUser) {
|
|
|
266
273
|
.limit(1);
|
|
267
274
|
if (existingLink) {
|
|
268
275
|
if (existingLink.userId === userId) {
|
|
269
|
-
// Already linked to this user
|
|
276
|
+
// Already linked to this user - refresh metadata and return
|
|
270
277
|
await db
|
|
271
278
|
.update(oauthAccounts)
|
|
272
279
|
.set({
|
|
@@ -281,7 +288,7 @@ export async function linkOAuthAccount(userId, provider, providerUser) {
|
|
|
281
288
|
throw new Error('Authenticated user not found in database');
|
|
282
289
|
return user;
|
|
283
290
|
}
|
|
284
|
-
// Linked to a different user
|
|
291
|
+
// Linked to a different user - cannot steal the identity
|
|
285
292
|
throw new Error('This provider account is already linked to another user. Unlink it from the other account first.');
|
|
286
293
|
}
|
|
287
294
|
// 2. Check the authenticated user exists
|
package/dist/server/passkey.d.ts
CHANGED
|
@@ -13,9 +13,9 @@ export interface PasskeyConfig {
|
|
|
13
13
|
maxPasskeysPerUser: number;
|
|
14
14
|
/** Challenge TTL in ms (default: 5 minutes) */
|
|
15
15
|
challengeTtlMs: number;
|
|
16
|
-
/** Relying Party ID
|
|
16
|
+
/** Relying Party ID - domain name (default: 'localhost') */
|
|
17
17
|
rpId: string;
|
|
18
|
-
/** Relying Party name
|
|
18
|
+
/** Relying Party name - user-visible (default: 'RevealUI') */
|
|
19
19
|
rpName: string;
|
|
20
20
|
/** Expected origin(s) for verification (default: 'http://localhost:4000') */
|
|
21
21
|
origin: string | string[];
|
|
@@ -93,7 +93,7 @@ export declare function verifyAuthentication(response: AuthenticationResponseJSO
|
|
|
93
93
|
newCounter: number;
|
|
94
94
|
}>;
|
|
95
95
|
/**
|
|
96
|
-
* List all passkeys for a user (safe for client
|
|
96
|
+
* List all passkeys for a user (safe for client - excludes publicKey and counter).
|
|
97
97
|
*/
|
|
98
98
|
export declare function listPasskeys(userId: string): Promise<{
|
|
99
99
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"passkey.d.ts","sourceRoot":"","sources":["../../src/server/passkey.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EACV,0BAA0B,EAC1B,sCAAsC,EACtC,qCAAqC,EACrC,wBAAwB,EACxB,4BAA4B,EAC5B,kBAAkB,EACnB,MAAM,wBAAwB,CAAC;AAahC,MAAM,WAAW,aAAa;IAC5B,8CAA8C;IAC9C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,+CAA+C;IAC/C,cAAc,EAAE,MAAM,CAAC;IACvB,
|
|
1
|
+
{"version":3,"file":"passkey.d.ts","sourceRoot":"","sources":["../../src/server/passkey.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EACV,0BAA0B,EAC1B,sCAAsC,EACtC,qCAAqC,EACrC,wBAAwB,EACxB,4BAA4B,EAC5B,kBAAkB,EACnB,MAAM,wBAAwB,CAAC;AAahC,MAAM,WAAW,aAAa;IAC5B,8CAA8C;IAC9C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,+CAA+C;IAC/C,cAAc,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC3B;AAaD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAGxE;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAgBD;;;;;;;;;;GAUG;AACH,wBAAsB,6BAA6B,CACjD,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,qBAAqB,CAAC,EAAE,MAAM,EAAE,GAC/B,OAAO,CAAC,sCAAsC,CAAC,CAwBjD;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,wBAAwB,EAClC,iBAAiB,EAAE,MAAM,EACzB,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GACjC,OAAO,CAAC,4BAA4B,CAAC,CAavC;AAMD;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE;IACV,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,UAAU,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,EACD,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC;IACT,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,IAAI,CAAC;CACjB,CAAC,CAqCD;AAMD;;;;;;GAMG;AACH,wBAAsB,+BAA+B,CACnD,gBAAgB,CAAC,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,EAAE,GACzD,OAAO,CAAC,qCAAqC,CAAC,CAqBhD;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,0BAA0B,EACpC,UAAU,EAAE,kBAAkB,EAC9B,iBAAiB,EAAE,MAAM,EACzB,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GACjC,OAAO,CAAC;IACT,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC,CAyBD;AAMD;;GAEG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CACzD;IACE,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;CACzB,EAAE,CACJ,CAgBA;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASpF;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC,CAkBzD"}
|
package/dist/server/passkey.js
CHANGED
|
@@ -199,7 +199,7 @@ export async function verifyAuthentication(response, credential, expectedChallen
|
|
|
199
199
|
// Management
|
|
200
200
|
// =============================================================================
|
|
201
201
|
/**
|
|
202
|
-
* List all passkeys for a user (safe for client
|
|
202
|
+
* List all passkeys for a user (safe for client - excludes publicKey and counter).
|
|
203
203
|
*/
|
|
204
204
|
export async function listPasskeys(userId) {
|
|
205
205
|
const db = getClient();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"password-reset.d.ts","sourceRoot":"","sources":["../../src/server/password-reset.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAuBD;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAwE5F;AAED;;;;;;;;;GASG;AACH,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAqCxB;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,mBAAmB,CAAC,
|
|
1
|
+
{"version":3,"file":"password-reset.d.ts","sourceRoot":"","sources":["../../src/server/password-reset.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAuBD;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAwE5F;AAED;;;;;;;;;GASG;AACH,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAqCxB;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,mBAAmB,CAAC,CAuF9B;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,MAAM,EACnB,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAiD/B;AAED;;;;;;;GAOG;AACH,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChG"}
|
|
@@ -37,7 +37,7 @@ function generateSalt() {
|
|
|
37
37
|
export async function generatePasswordResetToken(email) {
|
|
38
38
|
try {
|
|
39
39
|
const db = getClient();
|
|
40
|
-
// Find user by email
|
|
40
|
+
// Find user by email - intentionally does NOT check user.password.
|
|
41
41
|
// OAuth-only users (password: null) can use this flow to set a password,
|
|
42
42
|
// giving them a fallback login method independent of their OAuth provider.
|
|
43
43
|
// This is safe because the reset link is sent to their verified email.
|
|
@@ -166,7 +166,7 @@ export async function resetPasswordWithToken(tokenId, token, newPassword) {
|
|
|
166
166
|
};
|
|
167
167
|
}
|
|
168
168
|
// Validate password strength
|
|
169
|
-
const { validatePasswordStrength } = await import('./password-validation.js');
|
|
169
|
+
const { validatePasswordStrength, checkPasswordBreach } = await import('./password-validation.js');
|
|
170
170
|
const passwordValidation = validatePasswordStrength(newPassword);
|
|
171
171
|
if (!passwordValidation.valid) {
|
|
172
172
|
return {
|
|
@@ -174,6 +174,14 @@ export async function resetPasswordWithToken(tokenId, token, newPassword) {
|
|
|
174
174
|
error: passwordValidation.errors.join('. '),
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
|
+
// Check password against known data breaches (non-blocking on failure)
|
|
178
|
+
const breachCount = await checkPasswordBreach(newPassword);
|
|
179
|
+
if (breachCount > 0) {
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
error: `This password has appeared in ${breachCount.toLocaleString()} data breaches. Please choose a different password.`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
177
185
|
// Hash new password
|
|
178
186
|
const password = await bcrypt.hash(newPassword, 12);
|
|
179
187
|
// Update user password
|
|
@@ -242,7 +250,7 @@ export async function changePassword(userId, currentPassword, newPassword, curre
|
|
|
242
250
|
.where(and(eq(sessions.userId, userId), ne(sessions.id, currentSessionId)));
|
|
243
251
|
}
|
|
244
252
|
else {
|
|
245
|
-
// No current session ID provided
|
|
253
|
+
// No current session ID provided - delete all sessions as a safe default
|
|
246
254
|
await db.delete(sessions).where(eq(sessions.userId, userId));
|
|
247
255
|
}
|
|
248
256
|
return { success: true };
|
|
@@ -22,4 +22,15 @@ export declare function validatePasswordStrength(password: string): PasswordVali
|
|
|
22
22
|
* @returns True if meets minimum requirements
|
|
23
23
|
*/
|
|
24
24
|
export declare function meetsMinimumPasswordRequirements(password: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Check if a password appears in known data breaches via the
|
|
27
|
+
* HaveIBeenPwned Passwords API (k-anonymity model).
|
|
28
|
+
*
|
|
29
|
+
* Only the first 5 characters of the SHA-1 hash are sent to the API.
|
|
30
|
+
* The full hash never leaves the server.
|
|
31
|
+
*
|
|
32
|
+
* @returns The number of times this password has been seen in breaches, or 0 if clean.
|
|
33
|
+
* Returns -1 if the check could not be performed (network error).
|
|
34
|
+
*/
|
|
35
|
+
export declare function checkPasswordBreach(password: string): Promise<number>;
|
|
25
36
|
//# sourceMappingURL=password-validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"password-validation.d.ts","sourceRoot":"","sources":["../../src/server/password-validation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,wBAAwB,CA2BnF;AAED;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1E"}
|
|
1
|
+
{"version":3,"file":"password-validation.d.ts","sourceRoot":"","sources":["../../src/server/password-validation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,wBAAwB,CA2BnF;AAED;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1E;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2B3E"}
|
|
@@ -50,3 +50,40 @@ export function validatePasswordStrength(password) {
|
|
|
50
50
|
export function meetsMinimumPasswordRequirements(password) {
|
|
51
51
|
return password.length >= 8 && password.length <= 128;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Check if a password appears in known data breaches via the
|
|
55
|
+
* HaveIBeenPwned Passwords API (k-anonymity model).
|
|
56
|
+
*
|
|
57
|
+
* Only the first 5 characters of the SHA-1 hash are sent to the API.
|
|
58
|
+
* The full hash never leaves the server.
|
|
59
|
+
*
|
|
60
|
+
* @returns The number of times this password has been seen in breaches, or 0 if clean.
|
|
61
|
+
* Returns -1 if the check could not be performed (network error).
|
|
62
|
+
*/
|
|
63
|
+
export async function checkPasswordBreach(password) {
|
|
64
|
+
const { createHash } = await import('node:crypto');
|
|
65
|
+
// lgtm[js/insufficient-password-hash] - SHA-1 required by HIBP k-anonymity API, not used for password storage
|
|
66
|
+
const sha1 = createHash('sha1').update(password).digest('hex').toUpperCase();
|
|
67
|
+
const prefix = sha1.slice(0, 5);
|
|
68
|
+
const suffix = sha1.slice(5);
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`, {
|
|
71
|
+
headers: { 'Add-Padding': 'true' },
|
|
72
|
+
signal: AbortSignal.timeout(5000),
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok)
|
|
75
|
+
return -1;
|
|
76
|
+
const text = await response.text();
|
|
77
|
+
for (const line of text.split('\n')) {
|
|
78
|
+
const [hashSuffix, countStr] = line.split(':');
|
|
79
|
+
if (hashSuffix?.trim() === suffix) {
|
|
80
|
+
return Number.parseInt(countStr?.trim() ?? '0', 10);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Network error, timeout, or API issue - don't block the user
|
|
87
|
+
return -1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GitHub OAuth Provider
|
|
3
3
|
*
|
|
4
|
-
* Uses native fetch
|
|
4
|
+
* Uses native fetch - no additional npm dependencies.
|
|
5
5
|
* Scopes: read:user user:email
|
|
6
6
|
*
|
|
7
7
|
* Note: GitHub may return null email if user has set it private.
|
|
8
8
|
* In that case we fetch from /user/emails and pick the primary verified one.
|
|
9
9
|
*/
|
|
10
10
|
import type { ProviderUser } from '../oauth.js';
|
|
11
|
-
export declare function buildAuthUrl(clientId: string, redirectUri: string, state: string): string;
|
|
12
|
-
export declare function exchangeCode(code: string, redirectUri: string): Promise<string>;
|
|
11
|
+
export declare function buildAuthUrl(clientId: string, redirectUri: string, state: string, codeChallenge?: string): string;
|
|
12
|
+
export declare function exchangeCode(code: string, redirectUri: string, codeVerifier?: string): Promise<string>;
|
|
13
13
|
export declare function fetchUser(accessToken: string): Promise<ProviderUser>;
|
|
14
14
|
//# sourceMappingURL=github.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../../src/server/providers/github.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,YAAY,
|
|
1
|
+
{"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../../src/server/providers/github.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,CAWR;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAwCjB;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAkD1E"}
|
|
@@ -1,33 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GitHub OAuth Provider
|
|
3
3
|
*
|
|
4
|
-
* Uses native fetch
|
|
4
|
+
* Uses native fetch - no additional npm dependencies.
|
|
5
5
|
* Scopes: read:user user:email
|
|
6
6
|
*
|
|
7
7
|
* Note: GitHub may return null email if user has set it private.
|
|
8
8
|
* In that case we fetch from /user/emails and pick the primary verified one.
|
|
9
9
|
*/
|
|
10
|
-
export function buildAuthUrl(clientId, redirectUri, state) {
|
|
10
|
+
export function buildAuthUrl(clientId, redirectUri, state, codeChallenge) {
|
|
11
11
|
const url = new URL('https://github.com/login/oauth/authorize');
|
|
12
12
|
url.searchParams.set('client_id', clientId);
|
|
13
13
|
url.searchParams.set('redirect_uri', redirectUri);
|
|
14
14
|
url.searchParams.set('scope', 'read:user user:email');
|
|
15
15
|
url.searchParams.set('state', state);
|
|
16
|
+
if (codeChallenge) {
|
|
17
|
+
url.searchParams.set('code_challenge', codeChallenge);
|
|
18
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
19
|
+
}
|
|
16
20
|
return url.toString();
|
|
17
21
|
}
|
|
18
|
-
export async function exchangeCode(code, redirectUri) {
|
|
22
|
+
export async function exchangeCode(code, redirectUri, codeVerifier) {
|
|
23
|
+
const params = {
|
|
24
|
+
code,
|
|
25
|
+
client_id: process.env.GITHUB_CLIENT_ID ?? '',
|
|
26
|
+
client_secret: process.env.GITHUB_CLIENT_SECRET ?? '',
|
|
27
|
+
redirect_uri: redirectUri,
|
|
28
|
+
};
|
|
29
|
+
if (codeVerifier) {
|
|
30
|
+
params.code_verifier = codeVerifier;
|
|
31
|
+
}
|
|
19
32
|
const response = await fetch('https://github.com/login/oauth/access_token', {
|
|
20
33
|
method: 'POST',
|
|
21
34
|
headers: {
|
|
22
35
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
23
36
|
Accept: 'application/json',
|
|
24
37
|
},
|
|
25
|
-
body: new URLSearchParams(
|
|
26
|
-
code,
|
|
27
|
-
client_id: process.env.GITHUB_CLIENT_ID ?? '',
|
|
28
|
-
client_secret: process.env.GITHUB_CLIENT_SECRET ?? '',
|
|
29
|
-
redirect_uri: redirectUri,
|
|
30
|
-
}),
|
|
38
|
+
body: new URLSearchParams(params),
|
|
31
39
|
});
|
|
32
40
|
if (!response.ok) {
|
|
33
41
|
let detail = '';
|
|
@@ -36,9 +44,9 @@ export async function exchangeCode(code, redirectUri) {
|
|
|
36
44
|
detail = err.error_description ?? err.error ?? '';
|
|
37
45
|
}
|
|
38
46
|
catch {
|
|
39
|
-
// Response body not JSON
|
|
47
|
+
// Response body not JSON - use status only
|
|
40
48
|
}
|
|
41
|
-
throw new Error(`GitHub token exchange failed: ${response.status}${detail ? `
|
|
49
|
+
throw new Error(`GitHub token exchange failed: ${response.status}${detail ? ` - ${detail}` : ''}`);
|
|
42
50
|
}
|
|
43
51
|
const data = (await response.json());
|
|
44
52
|
if (data.error) {
|
|
@@ -64,7 +72,7 @@ export async function fetchUser(accessToken) {
|
|
|
64
72
|
catch {
|
|
65
73
|
// Response body not JSON
|
|
66
74
|
}
|
|
67
|
-
throw new Error(`GitHub user fetch failed: ${userResponse.status}${detail ? `
|
|
75
|
+
throw new Error(`GitHub user fetch failed: ${userResponse.status}${detail ? ` - ${detail}` : ''}`);
|
|
68
76
|
}
|
|
69
77
|
const user = (await userResponse.json());
|
|
70
78
|
let email = user.email ?? null;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Google OAuth 2.0 Provider
|
|
3
3
|
*
|
|
4
|
-
* Uses native fetch
|
|
4
|
+
* Uses native fetch - no additional npm dependencies.
|
|
5
5
|
* Scopes: openid email profile
|
|
6
6
|
*/
|
|
7
7
|
import type { ProviderUser } from '../oauth.js';
|
|
8
|
-
export declare function buildAuthUrl(clientId: string, redirectUri: string, state: string): string;
|
|
9
|
-
export declare function exchangeCode(code: string, redirectUri: string): Promise<string>;
|
|
8
|
+
export declare function buildAuthUrl(clientId: string, redirectUri: string, state: string, codeChallenge?: string): string;
|
|
9
|
+
export declare function exchangeCode(code: string, redirectUri: string, codeVerifier?: string): Promise<string>;
|
|
10
10
|
export declare function fetchUser(accessToken: string): Promise<ProviderUser>;
|
|
11
11
|
//# sourceMappingURL=google.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../../src/server/providers/google.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,YAAY,
|
|
1
|
+
{"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../../src/server/providers/google.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,CAaR;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAmCjB;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA+B1E"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Google OAuth 2.0 Provider
|
|
3
3
|
*
|
|
4
|
-
* Uses native fetch
|
|
4
|
+
* Uses native fetch - no additional npm dependencies.
|
|
5
5
|
* Scopes: openid email profile
|
|
6
6
|
*/
|
|
7
|
-
export function buildAuthUrl(clientId, redirectUri, state) {
|
|
7
|
+
export function buildAuthUrl(clientId, redirectUri, state, codeChallenge) {
|
|
8
8
|
const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');
|
|
9
9
|
url.searchParams.set('client_id', clientId);
|
|
10
10
|
url.searchParams.set('redirect_uri', redirectUri);
|
|
@@ -12,19 +12,27 @@ export function buildAuthUrl(clientId, redirectUri, state) {
|
|
|
12
12
|
url.searchParams.set('scope', 'openid email profile');
|
|
13
13
|
url.searchParams.set('state', state);
|
|
14
14
|
url.searchParams.set('access_type', 'online');
|
|
15
|
+
if (codeChallenge) {
|
|
16
|
+
url.searchParams.set('code_challenge', codeChallenge);
|
|
17
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
18
|
+
}
|
|
15
19
|
return url.toString();
|
|
16
20
|
}
|
|
17
|
-
export async function exchangeCode(code, redirectUri) {
|
|
21
|
+
export async function exchangeCode(code, redirectUri, codeVerifier) {
|
|
22
|
+
const params = {
|
|
23
|
+
code,
|
|
24
|
+
client_id: process.env.GOOGLE_CLIENT_ID ?? '',
|
|
25
|
+
client_secret: process.env.GOOGLE_CLIENT_SECRET ?? '',
|
|
26
|
+
redirect_uri: redirectUri,
|
|
27
|
+
grant_type: 'authorization_code',
|
|
28
|
+
};
|
|
29
|
+
if (codeVerifier) {
|
|
30
|
+
params.code_verifier = codeVerifier;
|
|
31
|
+
}
|
|
18
32
|
const response = await fetch('https://oauth2.googleapis.com/token', {
|
|
19
33
|
method: 'POST',
|
|
20
34
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
21
|
-
body: new URLSearchParams(
|
|
22
|
-
code,
|
|
23
|
-
client_id: process.env.GOOGLE_CLIENT_ID ?? '',
|
|
24
|
-
client_secret: process.env.GOOGLE_CLIENT_SECRET ?? '',
|
|
25
|
-
redirect_uri: redirectUri,
|
|
26
|
-
grant_type: 'authorization_code',
|
|
27
|
-
}),
|
|
35
|
+
body: new URLSearchParams(params),
|
|
28
36
|
});
|
|
29
37
|
if (!response.ok) {
|
|
30
38
|
let detail = '';
|
|
@@ -33,9 +41,9 @@ export async function exchangeCode(code, redirectUri) {
|
|
|
33
41
|
detail = err.error_description ?? err.error ?? '';
|
|
34
42
|
}
|
|
35
43
|
catch {
|
|
36
|
-
// Response body not JSON
|
|
44
|
+
// Response body not JSON - use status only
|
|
37
45
|
}
|
|
38
|
-
throw new Error(`Google token exchange failed: ${response.status}${detail ? `
|
|
46
|
+
throw new Error(`Google token exchange failed: ${response.status}${detail ? ` - ${detail}` : ''}`);
|
|
39
47
|
}
|
|
40
48
|
const data = (await response.json());
|
|
41
49
|
if (!data.access_token || typeof data.access_token !== 'string') {
|
|
@@ -56,7 +64,7 @@ export async function fetchUser(accessToken) {
|
|
|
56
64
|
catch {
|
|
57
65
|
// Response body not JSON
|
|
58
66
|
}
|
|
59
|
-
throw new Error(`Google userinfo fetch failed: ${response.status}${detail ? `
|
|
67
|
+
throw new Error(`Google userinfo fetch failed: ${response.status}${detail ? ` - ${detail}` : ''}`);
|
|
60
68
|
}
|
|
61
69
|
const data = (await response.json());
|
|
62
70
|
return {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Vercel OAuth Provider
|
|
3
3
|
*
|
|
4
|
-
* Uses native fetch
|
|
5
|
-
* No scopes required
|
|
4
|
+
* Uses native fetch - no additional npm dependencies.
|
|
5
|
+
* No scopes required - Vercel uses full access by default.
|
|
6
6
|
*/
|
|
7
7
|
import type { ProviderUser } from '../oauth.js';
|
|
8
|
-
export declare function buildAuthUrl(clientId: string, redirectUri: string, state: string): string;
|
|
9
|
-
export declare function exchangeCode(code: string, redirectUri: string): Promise<string>;
|
|
8
|
+
export declare function buildAuthUrl(clientId: string, redirectUri: string, state: string, _codeChallenge?: string): string;
|
|
9
|
+
export declare function exchangeCode(code: string, redirectUri: string, _codeVerifier?: string): Promise<string>;
|
|
10
10
|
export declare function fetchUser(accessToken: string): Promise<ProviderUser>;
|
|
11
11
|
//# sourceMappingURL=vercel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../../src/server/providers/vercel.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,YAAY,
|
|
1
|
+
{"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../../src/server/providers/vercel.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,MAAM,CAMR;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC,CAiCjB;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAmC1E"}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Vercel OAuth Provider
|
|
3
3
|
*
|
|
4
|
-
* Uses native fetch
|
|
5
|
-
* No scopes required
|
|
4
|
+
* Uses native fetch - no additional npm dependencies.
|
|
5
|
+
* No scopes required - Vercel uses full access by default.
|
|
6
6
|
*/
|
|
7
|
-
export function buildAuthUrl(clientId, redirectUri, state) {
|
|
7
|
+
export function buildAuthUrl(clientId, redirectUri, state, _codeChallenge) {
|
|
8
8
|
const url = new URL('https://vercel.com/oauth/authorize');
|
|
9
9
|
url.searchParams.set('client_id', clientId);
|
|
10
10
|
url.searchParams.set('redirect_uri', redirectUri);
|
|
11
11
|
url.searchParams.set('state', state);
|
|
12
12
|
return url.toString();
|
|
13
13
|
}
|
|
14
|
-
export async function exchangeCode(code, redirectUri) {
|
|
14
|
+
export async function exchangeCode(code, redirectUri, _codeVerifier) {
|
|
15
15
|
const response = await fetch('https://api.vercel.com/v2/oauth/access_token', {
|
|
16
16
|
method: 'POST',
|
|
17
17
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
@@ -29,9 +29,9 @@ export async function exchangeCode(code, redirectUri) {
|
|
|
29
29
|
detail = err.error_description ?? err.error ?? '';
|
|
30
30
|
}
|
|
31
31
|
catch {
|
|
32
|
-
// Response body not JSON
|
|
32
|
+
// Response body not JSON - use status only
|
|
33
33
|
}
|
|
34
|
-
throw new Error(`Vercel token exchange failed: ${response.status}${detail ? `
|
|
34
|
+
throw new Error(`Vercel token exchange failed: ${response.status}${detail ? ` - ${detail}` : ''}`);
|
|
35
35
|
}
|
|
36
36
|
const data = (await response.json());
|
|
37
37
|
if (data.error) {
|
|
@@ -55,7 +55,7 @@ export async function fetchUser(accessToken) {
|
|
|
55
55
|
catch {
|
|
56
56
|
// Response body not JSON
|
|
57
57
|
}
|
|
58
|
-
throw new Error(`Vercel user fetch failed: ${response.status}${detail ? `
|
|
58
|
+
throw new Error(`Vercel user fetch failed: ${response.status}${detail ? ` - ${detail}` : ''}`);
|
|
59
59
|
}
|
|
60
60
|
const data = (await response.json());
|
|
61
61
|
const u = data.user;
|
package/dist/server/session.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { Session, User } from '../types.js';
|
|
|
8
8
|
export interface SessionBindingConfig {
|
|
9
9
|
/** Invalidate session when user-agent changes (default: true) */
|
|
10
10
|
enforceUserAgent: boolean;
|
|
11
|
-
/** Invalidate session when IP address changes (default: false
|
|
11
|
+
/** Invalidate session when IP address changes (default: false - users roam) */
|
|
12
12
|
enforceIp: boolean;
|
|
13
13
|
/** Log a warning when IP changes but don't invalidate (default: true) */
|
|
14
14
|
warnOnIpChange: boolean;
|
|
@@ -36,7 +36,7 @@ export interface SessionData {
|
|
|
36
36
|
/**
|
|
37
37
|
* Check if a session is a recovery session (created via magic link recovery).
|
|
38
38
|
*
|
|
39
|
-
* Recovery sessions are restricted
|
|
39
|
+
* Recovery sessions are restricted - they should only be used for:
|
|
40
40
|
* - Changing the password (`/api/auth/change-password`)
|
|
41
41
|
* - Signing out (`/api/auth/sign-out`)
|
|
42
42
|
* - Viewing current session (`/api/auth/me`, `/api/auth/session`)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAQjD,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,gBAAgB,EAAE,OAAO,CAAC;IAC1B
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAQjD,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iFAAiF;IACjF,SAAS,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,cAAc,EAAE,OAAO,CAAC;CACzB;AAUD,mFAAmF;AACnF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,OAAO,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAEtF;AAED,qCAAqC;AACrC,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAMD,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CA0B3F;AAMD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAI1E;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAkH7B;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAmE9C;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,+FAA+F;IAC/F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAoC9C;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAoBtE;AAED;;;;GAIG;AACH;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE"}
|
package/dist/server/session.js
CHANGED
|
@@ -57,7 +57,7 @@ export function validateSessionBinding(session, ctx) {
|
|
|
57
57
|
/**
|
|
58
58
|
* Check if a session is a recovery session (created via magic link recovery).
|
|
59
59
|
*
|
|
60
|
-
* Recovery sessions are restricted
|
|
60
|
+
* Recovery sessions are restricted - they should only be used for:
|
|
61
61
|
* - Changing the password (`/api/auth/change-password`)
|
|
62
62
|
* - Signing out (`/api/auth/sign-out`)
|
|
63
63
|
* - Viewing current session (`/api/auth/me`, `/api/auth/session`)
|
|
@@ -127,7 +127,7 @@ export async function getSession(headers, requestContext) {
|
|
|
127
127
|
if (requestContext) {
|
|
128
128
|
const bindingError = validateSessionBinding(session, requestContext);
|
|
129
129
|
if (bindingError) {
|
|
130
|
-
logger.warn('Session binding violation
|
|
130
|
+
logger.warn('Session binding violation - invalidating session', {
|
|
131
131
|
sessionId: session.id,
|
|
132
132
|
reason: bindingError,
|
|
133
133
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signed-cookie.d.ts","sourceRoot":"","sources":["../../src/server/signed-cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EAC/D,OAAO,EAAE,CAAC,EACV,MAAM,EAAE,MAAM,GACb,MAAM,
|
|
1
|
+
{"version":3,"file":"signed-cookie.d.ts","sourceRoot":"","sources":["../../src/server/signed-cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EAC/D,OAAO,EAAE,CAAC,EACV,MAAM,EAAE,MAAM,GACb,MAAM,CASR;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EACjE,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,CAAC,GAAG,IAAI,CA0CV"}
|
|
@@ -18,6 +18,7 @@ import crypto from 'node:crypto';
|
|
|
18
18
|
export function signCookiePayload(payload, secret) {
|
|
19
19
|
const payloadJson = JSON.stringify(payload);
|
|
20
20
|
const payloadB64 = Buffer.from(payloadJson).toString('base64url');
|
|
21
|
+
// lgtm[js/insufficient-password-hash] - HMAC-SHA256 for cookie payload signing, not password hashing
|
|
21
22
|
const signature = crypto.createHmac('sha256', secret).update(payloadB64).digest();
|
|
22
23
|
const signatureB64 = signature.toString('base64url');
|
|
23
24
|
return `${payloadB64}.${signatureB64}`;
|
|
@@ -42,16 +43,17 @@ export function verifyCookiePayload(signed, secret) {
|
|
|
42
43
|
}
|
|
43
44
|
const [payloadB64, signatureB64] = parts;
|
|
44
45
|
// Recompute the expected signature
|
|
46
|
+
// lgtm[js/insufficient-password-hash] - HMAC-SHA256 for cookie verification, not password hashing
|
|
45
47
|
const expectedSignature = crypto.createHmac('sha256', secret).update(payloadB64).digest();
|
|
46
48
|
const actualSignature = Buffer.from(signatureB64, 'base64url');
|
|
47
|
-
// Timing-safe comparison
|
|
49
|
+
// Timing-safe comparison - buffers must be same length
|
|
48
50
|
if (expectedSignature.length !== actualSignature.length) {
|
|
49
51
|
return null;
|
|
50
52
|
}
|
|
51
53
|
if (!crypto.timingSafeEqual(expectedSignature, actualSignature)) {
|
|
52
54
|
return null;
|
|
53
55
|
}
|
|
54
|
-
// Signature valid
|
|
56
|
+
// Signature valid - decode and parse the payload
|
|
55
57
|
const payloadJson = Buffer.from(payloadB64, 'base64url').toString('utf8');
|
|
56
58
|
const payload = JSON.parse(payloadJson);
|
|
57
59
|
// Check expiry
|
|
@@ -39,7 +39,7 @@ export function getStorage() {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
catch {
|
|
42
|
-
// Config validation failed
|
|
42
|
+
// Config validation failed - try process.env fallback below
|
|
43
43
|
}
|
|
44
44
|
dbUrl = dbUrl || process.env.POSTGRES_URL || process.env.DATABASE_URL;
|
|
45
45
|
if (dbUrl) {
|
|
@@ -79,7 +79,7 @@ export function createStorage() {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
catch {
|
|
82
|
-
// Config validation failed
|
|
82
|
+
// Config validation failed - fall through to in-memory
|
|
83
83
|
}
|
|
84
84
|
return new InMemoryStorage();
|
|
85
85
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revealui/auth",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Authentication system for RevealUI - database-backed sessions with Better Auth patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -14,23 +14,23 @@
|
|
|
14
14
|
"bcryptjs": "^3.0.3",
|
|
15
15
|
"drizzle-orm": "^0.45.2",
|
|
16
16
|
"zod": "^4.3.6",
|
|
17
|
-
"@revealui/config": "0.3.
|
|
18
|
-
"@revealui/contracts": "1.3.
|
|
19
|
-
"@revealui/core": "0.5.
|
|
20
|
-
"@revealui/db": "0.3.
|
|
21
|
-
"@revealui/security": "0.2.
|
|
17
|
+
"@revealui/config": "0.3.4",
|
|
18
|
+
"@revealui/contracts": "1.3.7",
|
|
19
|
+
"@revealui/core": "0.5.6",
|
|
20
|
+
"@revealui/db": "0.3.7",
|
|
21
|
+
"@revealui/security": "0.2.7"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@simplewebauthn/browser": "^13.3.0",
|
|
25
25
|
"@testing-library/react": "^16.3.2",
|
|
26
|
-
"@types/node": "^25.5.
|
|
26
|
+
"@types/node": "^25.5.2",
|
|
27
27
|
"@types/react": "^19.2.14",
|
|
28
|
-
"@vitest/coverage-v8": "^4.1.
|
|
29
|
-
"happy-dom": "^20.8.
|
|
30
|
-
"react": "^19.2.
|
|
28
|
+
"@vitest/coverage-v8": "^4.1.3",
|
|
29
|
+
"happy-dom": "^20.8.9",
|
|
30
|
+
"react": "^19.2.5",
|
|
31
31
|
"typescript": "^6.0.2",
|
|
32
|
-
"vitest": "^4.1.
|
|
33
|
-
"dev": "0.0
|
|
32
|
+
"vitest": "^4.1.3",
|
|
33
|
+
"@revealui/dev": "0.1.0"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=24.13.0"
|
|
@@ -72,6 +72,16 @@
|
|
|
72
72
|
},
|
|
73
73
|
"type": "module",
|
|
74
74
|
"types": "./dist/index.d.ts",
|
|
75
|
+
"repository": {
|
|
76
|
+
"type": "git",
|
|
77
|
+
"url": "https://github.com/RevealUIStudio/revealui.git",
|
|
78
|
+
"directory": "packages/auth"
|
|
79
|
+
},
|
|
80
|
+
"homepage": "https://revealui.com",
|
|
81
|
+
"author": "RevealUI Studio <founder@revealui.com>",
|
|
82
|
+
"bugs": {
|
|
83
|
+
"url": "https://github.com/RevealUIStudio/revealui/issues"
|
|
84
|
+
},
|
|
75
85
|
"scripts": {
|
|
76
86
|
"build": "tsc",
|
|
77
87
|
"clean": "rm -rf dist",
|