Login

POST /v1/auth/login

Exchange an email (or username) and password for a JWT. The identifier is email-first: pass either your email address or your username in the username field — the server matches both and prefers an exact email match. The JWT is then sent in the Authorization: Bearer <token> header for subsequent requests; alternatively, mint an API key for long-running automation.

If the user has MFA enabled, this endpoint returns {"mfa_required": true, "mfa_challenge_token": "…"} with HTTP 200 instead of a JWT — no token is issued. Your client then calls POST /v1/auth/mfa with that mfa_challenge_token plus the TOTP code (the challenge is short-lived — see mfa_expires_in). Login is rate-limited per IP and per username; repeated failures return 429 with a lockout window.

Email verification is enforced — accounts with email_verified: false get 403 with code: "EMAIL_NOT_VERIFIED". If the user's active account enforces SSO for their email domain (via a domain-verified, enforced SSO config), password login is refused before the password is checked with 403 code: "SSO_REQUIRED" — the body carries sso_required, sso_provider, email_domain, and init_url so the client can redirect the user to their identity provider.

MFA still applies after SSO. If a user signs in through their identity provider but also has MFA enabled, they must complete the TOTP second factor before a session is minted — the SSO callback issues its own mfa_challenge_token for POST /v1/auth/mfa.

Request body

username string Req
Email address or username. The identifier is email-first — the server matches either and prefers an exact email match. (The JSON field is named username for historical reasons but accepts either value.)
password string Req
Plaintext password. Compared with the bcrypt hash server-side. Random delay (100-250ms) is added on failure to flatten timing-based username enumeration.

Response — success (200 OK)

token string
Signed JWT. Send as Authorization: Bearer <token> on subsequent requests. Default expiry is in the JWT's exp claim — usually 24h. The token also carries an active_account_id claim identifying the active account it is scoped to; to act in a different account, mint a new token via POST /v1/user/switch-account.
user object
User profile: id, username, email, plan, email_verified, plus per-plan limits (max_tunnels, max_endpoints, etc.) and timestamps.

Response — MFA required (200 OK)

mfa_required boolean
Always true. No token is issued — call /auth/mfa next.
mfa_challenge_token string
One-time, short-lived challenge token. Pass it (with the TOTP code) to /auth/mfa so the second factor doesn't re-take username/password. Only its hash is stored server-side.
mfa_expires_in integer
Seconds until mfa_challenge_token expires (currently 300 = 5 minutes). After that the client must log in again.
user object
Partial user profile (id, username, email, role) for UI hints — do not trust as authenticated until MFA succeeds.
message string
Human-readable hint, e.g. "MFA verification required".

Errors

400 Bad Request
Invalid JSON, missing username or password, or any unknown field (e.g. email or mfa_code — pass an email address in the username field, and send the TOTP code to /auth/mfa instead; the handler uses strict decoding).
401 Unauthorized
Wrong credentials. The same message ("Authentication failed") is used for both unknown user and wrong password to prevent enumeration.
403 Forbidden
Email not verified (code: "EMAIL_NOT_VERIFIED") — direct user to /auth/resend-verification. Or the user's active account enforces SSO for their email domain (code: "SSO_REQUIRED", with sso_required, sso_provider, email_domain, and init_url set) — send the user to init_url to start SSO.
429 Too Many Requests
Rate limit hit on the IP or username. Wait the Retry-After seconds before retrying.
503 Service Unavailable
The pre-auth SSO-enforcement lookup failed (code: "SSO_CHECK_UNAVAILABLE"). Transient — retry shortly.
Request
curl -X POST "https://api.ngris.com/v1/auth/login" \ -H "Content-Type: application/json" \ -d '{ "username": "alice", "password": "s3cur3p@ss!" }'
Python
from ngris import Ngris # api_key is irrelevant for /auth/login but the SDK requires one; # any non-empty value works. client = Ngris(api_key="placeholder") resp = client.auth.login(username="alice", password="s3cur3p@ss!") if resp.mfa_required: code = input("MFA code: ") resp = client.auth.mfa_verify("alice", "s3cur3p@ss!", code) print(resp.token)
Response — 200 OK
{ "token": "eyJhbGciOiJIUzI1NiI...", "user": { "id": 1024, "username": "alice", "email": "alice@example.com", "plan": "pro", "email_verified": true, "mfa_enabled": false, "max_endpoints": 20, "created_at": "2026-01-15T12:00:00Z", "last_login_at": "2026-04-29T12:34:56Z" } }
Response — MFA required
{ "success": false, "mfa_required": true, "mfa_challenge_token": "b3f1c2a9d4e5...", "mfa_expires_in": 300, "message": "MFA verification required", "user": { "id": 1024, "username": "alice", "email": "alice@example.com", "role": "user" } }
Error — email not verified
{ "error": "Please verify your email before logging in", "code": "EMAIL_NOT_VERIFIED", "user_id": 1024 }
Error — SSO required (403)
{ "error": "Password login is disabled for this domain. Please use SSO.", "code": "SSO_REQUIRED", "sso_required": true, "sso_provider": "oidc", "email_domain": "example.com", "init_url": "/auth/sso/init" }
On this page
Login
Iris