Skip to content

Authentication

JWT access tokens for the NextJudge API via email, password or OAuth and how to send them on data layer requests.

The data layer uses HS256 JWTs. Authenticated requests send the token in the Authorization header as the raw token string. No Bearer prefix. The web app follows the same convention.

Terminal window
curl http://localhost:5000/v1/users/YOUR_USER_ID \
-H "Authorization: eyJhbGciOiJIUzI1NiIs..."

If you add Bearer, the server parses that entire string as a JWT and returns Malformed JWT token.

The data layer fails at startup if these are missing (no auto-generated fallbacks):

VariablePurpose
JWT_SIGNING_SECRETSigns user and judge JWTs
JUDGE_PASSWORDJudge worker login (POST /v1/login_judge)
WEB_BRIDGE_SECRETWeb → API OAuth bridge (POST /v1/create_or_login_user)

Generate a local .env from the repo root:

Terminal window
./.createenv.sh > .env

Set the same WEB_BRIDGE_SECRET on the web app (src/web/.env.local). The web still accepts deprecated AUTH_PROVIDER_PASSWORD as a fallback; prefer WEB_BRIDGE_SECRET.

Auth is always enforced in production and in tests. There is no AUTH_DISABLED shortcut.

PathWho uses itEndpoint
Email/passwordScripts, curl, integrationsPOST /v1/basic_register, POST /v1/basic_login
OAuth (GitHub, etc.)Web app via NextAuthPOST /v1/create_or_login_user
Judge workerPython judge servicePOST /v1/login_judge

Most integrators use basic_login. The web app uses create_or_login_user after OAuth succeeds.

Terminal window
curl -X POST http://localhost:5000/v1/basic_register \
-H "Content-Type: application/json" \
-d '{
"name": "ada",
"email": "ada@example.com",
"password": "example-password"
}'

Success response:

{
"token": "eyJhbGciOiJIUzI1NiIs...",
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "ada",
"email": "ada@example.com",
"image": ""
}

Save token and id. Submissions require user_id in the body and the token in the header.

Login uses the same request shape:

Terminal window
curl -X POST http://localhost:5000/v1/basic_login \
-H "Content-Type: application/json" \
-d '{"email": "ada@example.com", "password": "example-password"}'

Wrong password returns 401 with {"error":"Invalid credentials","code":"INVALID_CREDENTIALS"}. The response does not indicate whether the email exists.

Set ADMIN_EMAILS in your env (comma-separated). Any account registered with a matching email gets is_admin: true and a JWT with admin role.

Terminal window
# .env.dev example
ADMIN_EMAILS=admin@example.com

There is no separate bootstrap step. Register with that email to receive admin access. In ./dev-deploy.sh, seed data may preload users; check the UI or query /v1/users as an authenticated admin.

Tokens are signed with JWT_SIGNING_SECRET. Claims:

ClaimTypeMeaning
idUUIDUser ID (nil UUID for judge tokens)
roleint0 = user, 1 = judge, 2 = admin

The middleware checks that the user still exists. Deleting an account invalidates existing tokens immediately.

After GitHub (or credentials) login, NextAuth calls:

Terminal window
curl -X POST http://localhost:5000/v1/create_or_login_user \
-H "Authorization: YOUR_WEB_BRIDGE_SECRET" \
-H "Content-Type: application/json" \
-d '{
"id": "github-12345",
"name": "ada",
"email": "ada@example.com",
"image": "https://avatars.githubusercontent.com/..."
}'

The Authorization value here is WEB_BRIDGE_SECRET, not a JWT. Only your web server should hold this secret. It proves the request came from your auth layer.

Response matches basic login: token, id, name, email.

Workers authenticate at startup (or per request internally):

Terminal window
curl -X POST http://localhost:5000/v1/login_judge \
-H "Authorization: YOUR_JUDGE_PASSWORD"

Returns {"token":"..."}. That token has role: 1 and can PATCH submissions and fetch test cases.

JUDGE_PASSWORD must match on both the data layer and judge containers. A mismatch leaves submissions in PENDING while RabbitMQ delivers messages to workers that cannot write results back.

Terminal window
POST /v1/basic_request_password_reset # body: {"email":"..."}
POST /v1/basic_reset_password # body: {"email":"...","new_password":"..."}

Both return {"status":"ok"} even if the email is not found (anti-enumeration). Reset accepts email and new password directly; there is no magic-link token yet.

Bearer eyJ... → 401 Malformed JWT. Omit the prefix.

Token works but POST /v1/problems returns 403 → admin role required. Check is_admin on your user or register with an ADMIN_EMAILS address.

401 User account no longer exists → account was deleted. Register again (new UUID).

Next: API reference for endpoints that consume these tokens.