Skip to content

feat: one shot fix auth - #24

Merged
Berget1411 merged 3 commits into
mainfrom
auth
Mar 29, 2026
Merged

feat: one shot fix auth#24
Berget1411 merged 3 commits into
mainfrom
auth

Conversation

@Berget1411

Copy link
Copy Markdown
Contributor

No description provided.

@greptile-apps

greptile-apps Bot commented Mar 29, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the previous session-cookie authentication system with a standard OAuth2 bearer JWT flow and introduces a shared-password "access gate" that locks the entire site (both the Next.js frontend and the FastAPI backend) behind a single password before users can reach the login page or any API endpoint.

Key changes:

  • Backend auth: security.py centralises JWT creation/verification (PyJWT HS256) and password hashing (pwdlib/argon2); auth.py now returns Token responses and exposes both /token (OAuth2 form) and /signin (JSON) endpoints; dependencies.py switches from cookie-session lookup to OAuth2PasswordBearer JWT validation.
  • Access gate: dev_access.py + routers/access_gate.py implement a password-protected gate; the FastAPI middleware blocks all routes except /api/access-gate/unlock when the gate is enabled; FastAPI issues a dev_access_granted cookie after a correct password is entered.
  • Frontend: lib/auth.ts adds a localStorage-backed JWT store and an authFetch wrapper; app/(protected)/layout.tsx server-checks the access-gate cookie and redirects to /dev-access if absent; all fetch calls across Chat, Sidebar, and Settings are migrated to authFetch.
  • One actionable issue found: the /api/access-gate/unlock endpoint compares the submitted password with Python's != operator instead of hmac.compare_digest, which is susceptible to timing attacks on the primary user-facing unlock path.

Confidence Score: 4/5

Safe to merge after replacing the != comparison in access_gate.py with hmac.compare_digest.

The overall architecture is sound and the implementation is clean. One P1 security issue remains: the primary password-check endpoint uses a non-constant-time comparison which is a timing-attack vector. Previously flagged issues (localStorage JWT, no-op signout, open redirect) are acknowledged trade-offs for a dev tool. The missing ?next= redirect is a minor UX gap.

backend/routers/access_gate.py — swap != for hmac.compare_digest on the password check before merging.

Important Files Changed

Filename Overview
backend/security.py New module centralising JWT creation/verification and password hashing (HS256 via PyJWT + pwdlib); implementation is clean with proper timezone-aware expiry and mandatory secret key validation.
backend/routers/auth.py Replaces session-cookie flow with OAuth2 bearer tokens; adds constant-time-safe dummy password verify on unknown users to prevent user-enumeration, and exposes both /token (form data) and /signin (JSON) endpoints.
backend/routers/access_gate.py New endpoint that validates the shared access password and sets the dev_access_granted cookie; uses Python's == operator rather than hmac.compare_digest, opening a timing-attack vector on the primary user-facing unlock path.
backend/dev_access.py Helper module for the access gate: computes SHA-256 cookie hash, reads env vars for cookie settings, and validates incoming requests via cookie or header; timing-attack issue on the raw header comparison was flagged in a previous review thread.
frontend/lib/auth.ts Provides localStorage-backed JWT storage and an authFetch wrapper that injects the Authorization header; localStorage XSS risk was flagged in a previous review thread.
frontend/app/(protected)/layout.tsx Server-side layout guard that redirects unauthenticated users to /dev-access; the redirect omits the ?next= query parameter so users are always sent to / after unlocking regardless of their intended destination.
frontend/app/dev-access/page.tsx New password entry page for the access gate; reads a next query param for post-unlock redirect but this param is never set by the application's own redirect flow (see layout.tsx).
frontend/lib/access-gate-server.ts Server-only helper that reads the dev_access_granted cookie and compares its SHA-256 hash against the configured password; correctly short-circuits when no password is configured.

Sequence Diagram

sequenceDiagram
    participant U as Browser
    participant NX as Next.js (Server)
    participant FA as FastAPI

    Note over U,FA: Dev Access Gate flow
    U->>NX: GET /  (no dev_access_granted cookie)
    NX->>NX: ProtectedLayout: hasAccessGateCookie() → false
    NX-->>U: redirect /dev-access
    U->>FA: POST /api/access-gate/unlock {password}
    FA->>FA: compare password (== operator ⚠️)
    FA-->>U: 200 OK + Set-Cookie: dev_access_granted=sha256
    U->>NX: GET /
    NX->>NX: hasAccessGateCookie() → true
    NX-->>U: 200 Render page

    Note over U,FA: JWT Auth flow
    U->>FA: POST /api/auth/token (form: username, password)
    FA->>FA: _authenticate_user(): verify password hash
    FA-->>U: {access_token, token_type: bearer}
    U->>U: localStorage.setItem(pyrmit_access_token, token)

    Note over U,FA: Authenticated API request
    U->>FA: GET /api/auth/me  Authorization: Bearer token
    FA->>FA: oauth2_scheme → decode_access_token → get_current_user
    FA-->>U: {id, name, email}

    Note over U,FA: Sign-out
    U->>FA: POST /api/auth/signout (Bearer token)
    FA-->>U: {message: Signed out} (token still valid ⚠️)
    U->>U: localStorage.removeItem(pyrmit_access_token)
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (3): Last reviewed commit: "feat: fix dev-access gate + remove nextj..." | Re-trigger Greptile


export default function DevAccessPage() {
const [password, setPassword] = useState('');
const [error, setError] = useState('');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Open redirect via unvalidated next parameter

The nextPath value is read directly from the URL query string and passed to router.replace() without any validation. If an attacker crafts a link like /dev-access?next=https://evil.com, a legitimate user who enters the correct password will be silently redirected to an external site. This is a classic open-redirect / phishing vector.

Only allow relative paths (those that begin with /):

Suggested change
const [error, setError] = useState('');
const rawNext = searchParams.get('next') ?? '/';
const nextPath = rawNext.startsWith('/') ? rawNext : '/';

Fix in Codex Fix in Claude Code Fix in Cursor

Comment thread frontend/lib/auth.ts
Comment on lines +1 to +14
const ACCESS_TOKEN_KEY = 'pyrmit_access_token';

export function getStoredAccessToken(): string | null {
if (typeof window === 'undefined') {
return null;
}
return window.localStorage.getItem(ACCESS_TOKEN_KEY);
}

export function storeAccessToken(token: string) {
window.localStorage.setItem(ACCESS_TOKEN_KEY, token);
}

export function clearAccessToken() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 JWT stored in localStorage is XSS-accessible

localStorage is readable by any JavaScript running on the page. If a dependency or user-generated content introduces an XSS vulnerability, the bearer token can be exfiltrated and used to impersonate the user until it expires. The conventional mitigation is to store the token in an httpOnly cookie (which JavaScript cannot read), similar to the previous session_token approach.

For a development tool this risk is lower, but if the app ever moves to production this will need to be revisited. Consider at minimum documenting the trade-off in a comment here so it isn't overlooked.

Fix in Codex Fix in Claude Code Fix in Cursor

Comment thread backend/routers/auth.py
Comment on lines +52 to 53
def signup(request: schemas.SignUpRequest, db: Session = Depends(get_db)):
existing_user = db.query(models.User).filter(models.User.email == request.email).first()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 signout is a no-op — issued tokens remain valid until expiry

The endpoint now returns a success response without doing anything server-side. Because JWTs are stateless, a token that was issued before logout is still cryptographically valid for up to ACCESS_TOKEN_EXPIRE_MINUTES minutes (30 by default). If a token is leaked or copied before logout, it can continue to be used after the user believes they've signed out.

Common mitigations include:

  • Maintaining a server-side token revocation/blocklist (e.g. a small Redis set of jti values)
  • Reducing the ACCESS_TOKEN_EXPIRE_MINUTES value significantly (e.g. 5–10 min) and pairing it with a longer-lived refresh token

For a development tool the current risk is acceptable, but it's worth documenting so consumers of this endpoint understand it doesn't actually invalidate the token.

Fix in Codex Fix in Claude Code Fix in Cursor

Comment thread backend/dev_access.py
@Berget1411

Copy link
Copy Markdown
Contributor Author

@greptile

Comment thread backend/routers/access_gate.py Outdated
Comment on lines +33 to +34
if submitted_password != configured_password:
raise HTTPException(status_code=401, detail="Invalid access password")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Non-constant-time comparison enables timing attack

!= short-circuits on the first mismatched character, leaking information about how many leading characters of the submitted password match the configured one. Since this is the primary user-facing endpoint for entering the password, an attacker can repeatedly call it and measure response latencies to brute-force the secret one character at a time.

Use hmac.compare_digest for a constant-time comparison — add import hmac at the top of the file and replace the check:

Suggested change
if submitted_password != configured_password:
raise HTTPException(status_code=401, detail="Invalid access password")
if not hmac.compare_digest(submitted_password, configured_password):

Fix in Codex Fix in Claude Code Fix in Cursor

@Berget1411
Berget1411 merged commit 7badaee into main Mar 29, 2026
1 check passed
@Berget1411
Berget1411 deleted the auth branch March 29, 2026 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant