Authentication

Authentication

Geena uses email-OTP login backed by Ed25519-signed JWTs with server-side session revocation. No passwords, no third-party identity provider.

Info

If your users authenticate through a partner company’s identity provider (SSO-style), see Partner OAuth Login instead — it describes the one-click OAuth2 flow for partner-managed users. The OTP flow below is for users who log in directly to Geena with an email address.

The flow

  1. Client calls POST /auth/send-otp with an email address. A 6-digit code is emailed to the user.
  2. Client calls POST /auth/verify-otp with the email + code. The server returns { token, expiresAt, isNewUser }.
  3. If isNewUser: true, the client calls the GraphQL user.register mutation to materialise the user row. Otherwise skip.
  4. Client stores the token and sends Authorization: Bearer <token> on every subsequent request.
  5. Before expiry, client calls POST /auth/renew to slide the session forward by another 72 hours and receive a fresh JWT.
Info

Sessions are 72 hours (3 days) sliding. OTP codes expire after 15 minutes. Max 3 OTP sends per email per hour; max 5 verification attempts per code.

POST /auth/send-otp

Public. Sends a one-time code to the given email.

curl -X POST https://api.test.geena.eu/auth/send-otp \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com"}'

Response — always 200 { "success": true }, regardless of whether the account exists. This prevents account enumeration.

Errors

  • 400 — invalid email format
  • 429 — rate limit exceeded (more than 3 sends in the last hour)
  • 503 — email service unavailable

POST /auth/verify-otp

Public. Exchanges the OTP code for a JWT and creates a session row.

curl -X POST https://api.test.geena.eu/auth/verify-otp \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","code":"123456"}'

Response

{
  "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...",
  "expiresAt": "2026-04-20T12:00:00Z",
  "isNewUser": true
}

Errors

  • 400 — missing email or code
  • 401 — invalid or expired code
  • 429 — too many verification attempts
Tip

If isNewUser: true, your next call must be mutation { user { register(input: {}) { userId } } } — without it the JWT is valid but there’s no user row for the KMS to operate on.

POST /auth/renew

Authenticated. Extends the current session by 72 hours and issues a new JWT with the refreshed expiry.

curl -X POST https://api.test.geena.eu/auth/renew \
  -H "Authorization: Bearer $TOKEN"

Response

{ "token": "eyJhbGciOi...", "expiresAt": "2026-04-20T12:00:00Z" }

Secondary email verification

Some organization flows require the user to prove control of an email other than the one they logged in with (e.g. domain verification).

POST /auth/verify-email/send

Authenticated. Sends an OTP to the secondary email (re-uses the same OTP infrastructure; same rate limits apply).

curl -X POST https://api.test.geena.eu/auth/verify-email/send \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@company.com"}'

POST /auth/verify-email/confirm

Authenticated. Verifies the OTP and attaches the newly verified email to the current session.

curl -X POST https://api.test.geena.eu/auth/verify-email/confirm \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@company.com","code":"123456"}'

Response{ "verified": true }.

Session revocation

Sessions can be revoked server-side via the user.security mutations (revokeSession, revokeAllSessions, revokeAllSessionsIncludingCurrent). A revoked session’s JWT will fail auth verification on the next request.