feat: custom login UI Phase 1 — username + password flow#38
feat: custom login UI Phase 1 — username + password flow#38JohnRDOrazio wants to merge 1 commit intodevfrom
Conversation
Implement Phase 1 of the custom login UI to replace the Zitadel Login V2 container. This allows login pages to inherit the app's dark/light theme. Server-side auth logic: - lib/auth/zitadel-session.ts: Server Actions for Zitadel Session API v2 (createSession, verifyPassword, listAuthMethods, finalizeAuthRequest) - lib/auth/login-session.ts: Encrypted cookie helpers using JWE (A256GCM) with AUTH_SECRET for login state between steps Shared components: - components/auth/AuthCard.tsx: Centered card with OntoKit branding - components/auth/AuthInput.tsx: Styled input with label and error state - components/auth/BackLink.tsx: Back navigation between login steps Login pages: - app/auth/login/layout.tsx: Shared centered layout - app/auth/login/page.tsx: Username/email entry (reads ?authRequest= param) - app/auth/login/password/page.tsx: Password verification with MFA check Also adds ZITADEL_LOGIN_PAT to env schema and .env.example. Closes #3 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 9 minutes and 49 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (11)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| const response = await fetch(`${ZITADEL_ISSUER}/v2/sessions/${sessionId}`, { | ||
| method: "GET", | ||
| headers: { | ||
| ...zitadelHeaders(), | ||
| "x-zitadel-session-token": sessionToken, | ||
| }, | ||
| }); |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, to mitigate SSRF when constructing URLs from user input, you (a) ensure that the origin (scheme + host + port) is not user-controlled, and (b) strictly validate or normalize any user-controlled path segments so they cannot inject extra slashes, .., or other unexpected characters. Using an allow‑list of acceptable patterns is preferable to trying to sanitize arbitrary strings.
For this specific case, we should validate sessionId before using it in the URL path. Zitadel session IDs are opaque strings but are typically safe to treat as URL-safe tokens. A conservative fix is to restrict sessionId to a safe character set (e.g., alphanumerics, -, _, ., ~) and optionally a reasonable length. If the value does not match, we throw an error instead of issuing the HTTP request. This keeps existing functionality for valid IDs but blocks malformed or attacker-crafted paths.
Concretely, in lib/auth/zitadel-session.ts:
- Add a small helper function, e.g.,
validateSessionId, near the other helpers in this file. It checkssessionIdagainst a regular expression like/^[A-Za-z0-9._~-]{1,256}$/and throws an error if invalid. - Call
validateSessionId(sessionId)at the start ofgetSessionand at the start ofverifyPassword(or at least once early in the flow before any network request uses it). This ensures all uses ofsessionIdare constrained, including the internal call fromverifyPasswordtogetSession. - No external dependencies are required; TypeScript/JavaScript regexes suffice.
This approach does not change successful behavior for legitimate session IDs, but it prevents malformed values from reaching fetch and addresses the CodeQL SSRF concern.
| @@ -3,6 +3,17 @@ | ||
| const ZITADEL_ISSUER = process.env.ZITADEL_ISSUER || "http://localhost:8080"; | ||
| const ZITADEL_LOGIN_PAT = process.env.ZITADEL_LOGIN_PAT || ""; | ||
|
|
||
| /** | ||
| * Validate session IDs before using them in URLs to avoid SSRF-style issues. | ||
| * Allows URL-safe characters only and limits length to a reasonable size. | ||
| */ | ||
| function validateSessionId(sessionId: string): void { | ||
| const SESSION_ID_REGEX = /^[A-Za-z0-9._~-]{1,256}$/; | ||
| if (!SESSION_ID_REGEX.test(sessionId)) { | ||
| throw new Error("Invalid session ID format"); | ||
| } | ||
| } | ||
|
|
||
| interface ZitadelSessionResponse { | ||
| sessionId: string; | ||
| sessionToken: string; | ||
| @@ -108,6 +119,7 @@ | ||
| * Get session details. | ||
| */ | ||
| async function getSession(sessionId: string, sessionToken: string): Promise<ZitadelSession> { | ||
| validateSessionId(sessionId); | ||
| const response = await fetch(`${ZITADEL_ISSUER}/v2/sessions/${sessionId}`, { | ||
| method: "GET", | ||
| headers: { | ||
| @@ -136,6 +148,7 @@ | ||
| sessionToken: string; | ||
| factors: ZitadelSession["session"]["factors"]; | ||
| }> { | ||
| validateSessionId(sessionId); | ||
| const response = await fetch(`${ZITADEL_ISSUER}/v2/sessions/${sessionId}`, { | ||
| method: "PATCH", | ||
| headers: zitadelHeaders(), |
| const response = await fetch(`${ZITADEL_ISSUER}/v2/sessions/${sessionId}`, { | ||
| method: "PATCH", | ||
| headers: zitadelHeaders(), | ||
| body: JSON.stringify({ | ||
| sessionToken, | ||
| checks: { | ||
| password: { | ||
| password, | ||
| }, | ||
| }, | ||
| }), | ||
| }); |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
To fix this, we should constrain the sessionId parameter before using it in the request URL. Since the hostname is already fixed, the main remaining risk is path manipulation or unexpected characters in sessionId. A robust mitigation is to validate sessionId against a strict pattern that matches the format used by Zitadel session IDs (or, if that format is not known, a conservative alphanumeric/UUID-like pattern), and reject or error out if the value does not conform. This ensures that only well-formed identifiers are used in the path, preventing an attacker from inserting /, \, ?, #, or .. sequences.
The best way to fix the problem without changing existing functionality is:
- Introduce a small helper function, e.g.
validateSessionId(sessionId: string): void, near the top oflib/auth/zitadel-session.ts. - This function should check
sessionIdagainst a whitelist regex allowing only expected characters (for example,[A-Za-z0-9_-]+), and throw an error if it fails. - Call
validateSessionId(sessionId);at the start of any function that ultimately usessessionIdin a request URL, specificallyverifyPassword(and optionally alsogetSessionfor consistency). - The rest of the logic (fetch, response handling) remains unchanged, so existing, valid IDs continue to work.
No external libraries are necessary; a simple regex-based validation is sufficient. All changes are confined to lib/auth/zitadel-session.ts inside the shown regions.
| @@ -60,6 +60,15 @@ | ||
| }; | ||
| } | ||
|
|
||
| function validateSessionId(sessionId: string): void { | ||
| // Allow only conservative, expected characters in session IDs. | ||
| // Adjust this pattern if Zitadel uses a more specific format. | ||
| const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/; | ||
| if (!SESSION_ID_PATTERN.test(sessionId)) { | ||
| throw new Error("Invalid session identifier format"); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Create a new Zitadel session with a login name (username or email). | ||
| * This is step 1 of the login flow. | ||
| @@ -108,6 +117,7 @@ | ||
| * Get session details. | ||
| */ | ||
| async function getSession(sessionId: string, sessionToken: string): Promise<ZitadelSession> { | ||
| validateSessionId(sessionId); | ||
| const response = await fetch(`${ZITADEL_ISSUER}/v2/sessions/${sessionId}`, { | ||
| method: "GET", | ||
| headers: { | ||
| @@ -136,6 +146,7 @@ | ||
| sessionToken: string; | ||
| factors: ZitadelSession["session"]["factors"]; | ||
| }> { | ||
| validateSessionId(sessionId); | ||
| const response = await fetch(`${ZITADEL_ISSUER}/v2/sessions/${sessionId}`, { | ||
| method: "PATCH", | ||
| headers: zitadelHeaders(), |
| const response = await fetch( | ||
| `${ZITADEL_ISSUER}/v2/users/${userId}/authentication_methods`, | ||
| { | ||
| method: "GET", | ||
| headers: zitadelHeaders(), | ||
| } | ||
| ); |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
Copilot Autofix
AI about 1 month ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
Summary
Implements Phase 1 of the custom login UI (#3) to replace the Zitadel Login V2 container. Login pages now live inside the OntoKit frontend, inheriting the app's dark/light theme automatically.
lib/auth/zitadel-session.ts):createSession,verifyPassword,listAuthMethods,finalizeAuthRequest— all calls made server-side using the login-client PATlib/auth/login-session.ts): JWE (A256GCM) encrypted HTTP-only cookie stores login state between steps (10 min TTL)AuthCard,AuthInput,BackLink— reusable across all auth pages/auth/login?authRequest=<id>) and password verification (/auth/login/password)/auth/login/mfa(Phase 3)ZITADEL_LOGIN_PATenv var to schema and.env.exampleArchitecture
Remaining phases (future PRs)
Test plan
compose.yamlto point Zitadel login URLs tohttp://localhost:3000/auth/loginZITADEL_LOGIN_PATin.env.localCloses #3
🤖 Generated with Claude Code