Skip to content

Conversation

@hlbmtc
Copy link
Contributor

@hlbmtc hlbmtc commented Jan 16, 2026

Details -- https://www.notion.so/metaculus/Auth-migration-roadmap-2e96aaf4f10180728140ddbdb51f5045?source=copy_link#2e96aaf4f10180599e5acc246d55a4ba

Part of #3977

Summary by CodeRabbit

  • New Features

    • JWT access+refresh auth with cookie-managed sessions, token refresh endpoint, single-flight client refresh, bot JWT issuance, and a deprecated legacy-token exchange.
  • Bug Fixes / UX

    • Auth flows (login/signup/activate/reset/social) now return "tokens", support proactive refresh and automatic 401 retry.
  • Chores

    • Migrated middleware, fetch, and front-end flows to the cookie-based auth manager; removed legacy server-session helpers; added JWT config.
  • Tests

    • Auth tests updated to expect "tokens" responses.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

Warning

Rate limit exceeded

@hlbmtc has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 23 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 25ee82c and 9c01a19.

📒 Files selected for processing (1)
  • front_end/src/middleware.ts
📝 Walkthrough

Walkthrough

Replaces ApiKey/server-session auth with JWT access/refresh tokens across backend and frontend: adds get_tokens_for_user, Simple JWT config and refresh endpoint; introduces AuthCookieManager, single‑flight refresh, legacy migration, middleware changes, proxy/header updates, API client updates, types, and test adjustments.

Changes

Cohort / File(s) Summary
Backend JWT Core
authentication/services.py, metaculus_web/settings.py, pyproject.toml
Added get_tokens_for_user(user); configured Simple JWT (access 15m, refresh 30d, rotation/revocation, HS/RS fallback); added dependencies djangorestframework-simplejwt and cryptography.
Backend Endpoints & Flows
authentication/urls.py, authentication/views/common.py, authentication/views/social.py
Added /auth/refresh/ and legacy /auth/exchange-legacy-token/; replaced token ApiKey responses with JWT tokens produced by get_tokens_for_user(); social flows switched to JWTAuthentication and serializers/fields renamed to tokens.
Backend Bot JWT
users/urls.py, users/views.py
New POST users/me/bots/<id>/jwt/ (bot_jwt_api_view) that returns bot JWT tokens via get_tokens_for_user().
Backend Cleanup
projects/admin.py
Removed ApiKey creation in test user generation flows.
Frontend Auth Cookie Manager & Types
front_end/src/services/auth_tokens.ts, front_end/src/types/auth.ts
New AuthCookieReader/AuthCookieManager, cookie names/options, JWT helpers, expiry logic; AuthTokens type added; getAuthCookieManager() factory exported.
Frontend Single‑Flight Refresh
front_end/src/services/auth_refresh.ts
Added refreshWithSingleFlight(refreshToken) to deduplicate concurrent refresh requests and return `AuthTokens
Frontend Legacy Migration
front_end/src/services/auth_tokens_migration.ts
Added handleLegacyTokenMigration() to exchange deprecated auth_token cookie for new tokens (deprecated helper).
Frontend Session Removal
front_end/src/services/session.ts
Removed server-session helpers (getServerSession, setServerSession, impersonator helpers); retained alpha-token helpers and added setAlphaTokenSession.
Frontend Middleware
front_end/src/middleware.ts
Reworked middleware to use AuthCookieReader/Manager; added refreshTokensIfNeeded() and verifyToken(); integrates legacy migration, proactive refresh, and response cookie handling; adjusted route matching.
Frontend API Proxy
front_end/src/app/(api)/api-proxy/[...path]/route.ts
Proxy now uses getAuthCookieManager(), calls refreshWithSingleFlight() before requests, builds Authorization headers, retries on 401, and writes refreshed tokens to cookies.
Frontend Pages & Actions
front_end/src/app/(main)/** (multiple files)
Replaced getServerSession/setServerSession with getAuthCookieManager() and setAuthTokens()/hasAuthSession() across login, signup, activation, social OAuth, password reset, impersonation, profile/language, and AIB pages.
Frontend API Client & Fetch Utilities
front_end/src/services/api/*, front_end/src/utils/core/fetch/*, front_end/src/utils/core/fetch/fetch.server.ts
Extended POST signatures to accept optional FetchConfig; added ServerAuthApi.refreshTokens() and ServerProfileApi.getBotJwt(); introduced serverFetch() that injects auth header from AuthCookieManager.
Frontend Tests
tests/unit/test_auth/test_views.py
Updated tests to expect tokens instead of token in signup responses.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Browser/Client
    participant APIProxy as API Proxy Route
    participant AuthMgr as AuthCookieManager
    participant RefreshSvc as refreshWithSingleFlight
    participant Backend as Backend API

    Client->>APIProxy: Request to /api/...
    APIProxy->>AuthMgr: getAuthCookieManager()
    AuthMgr->>AuthMgr: read access/refresh tokens from cookies
    APIProxy->>RefreshSvc: refreshWithSingleFlight(refreshToken)
    alt Refresh performed
        RefreshSvc->>Backend: POST /auth/refresh/
        Backend->>RefreshSvc: New tokens
        RefreshSvc->>AuthMgr: setAuthTokens(newTokens)
    else Cached / no-op
        RefreshSvc-->>APIProxy: null or cached result
    end
    APIProxy->>APIProxy: buildHeaders(accessToken)
    APIProxy->>Backend: Proxied request with Authorization header
    alt Response 401
        APIProxy->>RefreshSvc: refreshWithSingleFlight(refreshToken)
        RefreshSvc->>Backend: POST /auth/refresh/
        Backend->>RefreshSvc: New tokens
        APIProxy->>Backend: Retry request with new token
    end
    Backend->>APIProxy: Response
    APIProxy->>AuthMgr: Write refreshed tokens to cookies
    APIProxy->>Client: Response
Loading
sequenceDiagram
    participant Req as Incoming Request
    participant Middleware as Middleware
    participant AuthReader as AuthCookieReader
    participant AuthMgr as AuthCookieManager
    participant ServerAuth as ServerAuthApi
    participant Backend as Backend API

    Req->>Middleware: Next.js Request
    Middleware->>AuthReader: read tokens from request cookies
    Middleware->>Middleware: handleLegacyTokenMigration()
    alt Legacy token swapped
        Middleware->>Backend: POST /auth/exchange-legacy-token/
        Backend->>Middleware: New JWT tokens
        Middleware->>AuthMgr: setAuthTokens(newTokens)
    end
    Middleware->>Middleware: refreshTokensIfNeeded()
    alt Access expired & refresh exists
        Middleware->>ServerAuth: POST /auth/refresh/
        ServerAuth->>Backend: validate & rotate
        Backend->>ServerAuth: New tokens
        Middleware->>AuthMgr: setAuthTokens(newTokens)
    else Verify token via backend
        Middleware->>Backend: token verification request
    end
    Middleware->>AuthMgr: Write final tokens to response cookies
    Middleware->>Req: Continue to app
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • elisescu
  • lsabor

Poem

🐰✨ Tokens hop from old keys to new,
Cookies cradle access and refresh too.
Middleware hums and proxies queue,
Single‑flight keeps refreshes few.
A rabbit cheers — secure hops, and woo!

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Auth Integration V1' is vague and generic, using non-descriptive terms that don't clearly convey the specific changes in the changeset. Consider a more descriptive title that highlights the main change, such as 'Migrate authentication to JWT-based refresh token flow' or 'Implement JWT refresh token authentication system'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@hlbmtc hlbmtc changed the title JWT Integration Auth Integration V1 Jan 16, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@metaculus_web/settings.py`:
- Around line 150-155: DEFAULT_AUTHENTICATION_CLASSES currently lists
"rest_framework.authentication.SessionAuthentication" before
"rest_framework_simplejwt.authentication.JWTAuthentication", which causes
SessionAuthentication to run first and block JWT auth (and raise CSRF errors);
reorder the list so "rest_framework_simplejwt.authentication.JWTAuthentication"
appears before "rest_framework.authentication.SessionAuthentication" in
DEFAULT_AUTHENTICATION_CLASSES (leaving
"authentication.auth.FallbackTokenAuthentication" as-is) so JWTAuthentication is
attempted prior to SessionAuthentication.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 856c825 and a2ed00d.

📒 Files selected for processing (1)
  • metaculus_web/settings.py
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-01-15T19:29:58.940Z
Learnt from: hlbmtc
Repo: Metaculus/metaculus PR: 4075
File: authentication/urls.py:24-26
Timestamp: 2026-01-15T19:29:58.940Z
Learning: In this codebase, DRF is configured to use IsAuthenticated as the default in REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] within metaculus_web/settings.py. Therefore, explicit permission_classes([IsAuthenticated]) decorators are unnecessary on DRF views unless a view needs to override the default. When reviewing Python files, verify that views relying on the default are not redundantly decorated, and flag cases where permissions are being over-specified or when a non-default permission is explicitly required.

Applied to files:

  • metaculus_web/settings.py
📚 Learning: 2026-01-16T20:30:15.454Z
Learnt from: hlbmtc
Repo: Metaculus/metaculus PR: 4087
File: authentication/views/social.py:47-55
Timestamp: 2026-01-16T20:30:15.454Z
Learning: In the Metaculus codebase, `SocialTokenOnlyAuthView` from the `rest_social_auth` library uses `AllowAny` permission by default, so views inheriting from it (like `SocialCodeAuth` in `authentication/views/social.py`) do not need to explicitly set `permission_classes = (AllowAny,)` for OAuth code exchange to work with unauthenticated requests.

Applied to files:

  • metaculus_web/settings.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: integration-tests
  • GitHub Check: Frontend Checks
  • GitHub Check: Backend Checks
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
metaculus_web/settings.py (2)

16-23: Imports look fine for the JWT additions.
No concerns here.


166-201: [rewritten comment]
[exactly one classification tag]

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +171 to +191
def get_jwt_encryption_config():
private_key_pem = os.environ.get("JWT_PRIVATE_KEY", "").replace("\\n", "\n")

if not private_key_pem:
# Fallback to HS256 with SECRET_KEY
return {"ALGORITHM": "HS256", "SIGNING_KEY": SECRET_KEY, "VERIFYING_KEY": None}

private_key = load_pem_private_key(private_key_pem.encode(), password=None)
public_key_pem = (
private_key.public_key()
.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
.decode()
)
return {
"ALGORITHM": "RS256",
"SIGNING_KEY": private_key_pem,
"VERIFYING_KEY": public_key_pem,
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please note: we’ll use our custom RS256 private key in production for asymmetric encryption. If JWT_PRIVATE_KEY isn’t specified, it will fall back to the Django SECRET_KEY.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@metaculus_web/settings.py`:
- Around line 22-23: The settings module imports cryptography symbols
(serialization, load_pem_private_key) but cryptography is only pulled in as an
optional extra of djangorestframework-simplejwt; add cryptography as an explicit
direct dependency in pyproject.toml under [tool.poetry.dependencies] (or
alternatively pin djangorestframework-simplejwt with the crypto extra, e.g.,
djangorestframework-simplejwt[crypto]) so that imports in metaculus_web.settings
(serialization, load_pem_private_key) will always succeed at import time.
♻️ Duplicate comments (1)
metaculus_web/settings.py (1)

149-155: JWT should authenticate before SessionAuthentication to avoid CSRF blocks.

Line 150–154: when SessionAuthentication runs first, it can enforce CSRF and short‑circuit JWT auth for SPA calls that include a session cookie. Reorder so JWT runs first.

♻️ Proposed reordering
 "DEFAULT_AUTHENTICATION_CLASSES": [
-    "rest_framework.authentication.SessionAuthentication",
-    # Primary auth mechanism for web users
-    "rest_framework_simplejwt.authentication.JWTAuthentication",
+    # Primary auth mechanism for web users
+    "rest_framework_simplejwt.authentication.JWTAuthentication",
+    "rest_framework.authentication.SessionAuthentication",
     # Auth Token: should be used for bots only!
     "authentication.auth.FallbackTokenAuthentication",
 ],
🧹 Nitpick comments (1)
metaculus_web/settings.py (1)

171-201: Add a clearer failure mode for invalid JWT_PRIVATE_KEY.

Line 171–191: if JWT_PRIVATE_KEY is set but malformed/encrypted, settings import will crash with a low‑signal error. Consider raising ImproperlyConfigured with a clear message.

🛠️ Suggested hardening
-from django.core.exceptions import DisallowedHost
+from django.core.exceptions import DisallowedHost, ImproperlyConfigured
 ...
 def get_jwt_encryption_config():
     private_key_pem = os.environ.get("JWT_PRIVATE_KEY", "").replace("\\n", "\n")

     if not private_key_pem:
         # Fallback to HS256 with SECRET_KEY
         return {"ALGORITHM": "HS256", "SIGNING_KEY": SECRET_KEY, "VERIFYING_KEY": None}

-    private_key = load_pem_private_key(private_key_pem.encode(), password=None)
+    try:
+        private_key = load_pem_private_key(private_key_pem.encode(), password=None)
+    except Exception as exc:
+        raise ImproperlyConfigured(
+            "JWT_PRIVATE_KEY must be a valid PEM-encoded RSA private key."
+        ) from exc
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2ed00d and 04aef4b.

📒 Files selected for processing (1)
  • metaculus_web/settings.py
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-01-15T19:29:58.940Z
Learnt from: hlbmtc
Repo: Metaculus/metaculus PR: 4075
File: authentication/urls.py:24-26
Timestamp: 2026-01-15T19:29:58.940Z
Learning: In this codebase, DRF is configured to use IsAuthenticated as the default in REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] within metaculus_web/settings.py. Therefore, explicit permission_classes([IsAuthenticated]) decorators are unnecessary on DRF views unless a view needs to override the default. When reviewing Python files, verify that views relying on the default are not redundantly decorated, and flag cases where permissions are being over-specified or when a non-default permission is explicitly required.

Applied to files:

  • metaculus_web/settings.py
📚 Learning: 2026-01-16T20:30:15.454Z
Learnt from: hlbmtc
Repo: Metaculus/metaculus PR: 4087
File: authentication/views/social.py:47-55
Timestamp: 2026-01-16T20:30:15.454Z
Learning: In the Metaculus codebase, `SocialTokenOnlyAuthView` from the `rest_social_auth` library uses `AllowAny` permission by default, so views inheriting from it (like `SocialCodeAuth` in `authentication/views/social.py`) do not need to explicitly set `permission_classes = (AllowAny,)` for OAuth code exchange to work with unauthenticated requests.

Applied to files:

  • metaculus_web/settings.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: integration-tests

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Co-authored-by: Nikita <93587872+ncarazon@users.noreply.github.com>
Copy link
Contributor

@cemreinanc cemreinanc left a comment

Choose a reason for hiding this comment

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

LGTM

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.

4 participants