Logging in with email & password
Signing inThe base login: email + password, on a browser this IAM already recognises, with no second factor enrolled (the LE2 path). Whether MFA or a device challenge applies is not something the client can check beforehand — the login response itself reveals the branch.
- POST /v1/auth/login with { identifier: <email>, password } → known device + no MFA: accessToken + refresh cookie (amr: [pwd])
- Other answers: { challengeToken, challenge, remaining } → scenario 11 / 12 / 13 · { mfaRequired, mfaToken } (legacy shape) → scenario 12
/v1/auth/loginpublicLog in (identifier + password)Submit { identifier, password } where identifier is the email ({ email } is still accepted as a deprecated alias and normalised server-side). ON A RECOGNISED BROWSER SEND ONLY { password }: with a device_token cookie present identifier is optional and the account is resolved from the trusted device, which is what lets the “Welcome back” screen (scenario 8) avoid asking for an identifier it can only display masked. No identifier AND no usable cookie is a uniform 401, never a 422 “identifier is required” — that message would itself confirm the cookie was rejected. The account lockout still applies, keyed on the RESOLVED identifier. Wrong credentials, an unverified credential, or an inactive account all return a uniform 401 invalid_credentials — the cause is never disclosed, and the password path spends the same argon2 work either way so latency does not leak account existence. The credential you type must itself be VERIFIED. clientId is reserved and currently rejected (422). No active tenant membership → 403; multiple tenants → the DEFAULT is picked (use switch-tenant, scenario 20).
Field guide — what each value means & where it comes from4 fields
Public, unauthenticated login. No Authorization header; send only the JSON body below. Throttled per credential/IP; response is no-store. The answer is one of three shapes — tokens, a challenge chain, or the legacy MFA challenge — and which one you get is NOT checkable beforehand (there is no pre-login MFA-status endpoint, by anti-enumeration design). Before the credential is examined at all, the active LOGIN policy is checked: if the channel this attempt uses is off, the answer is 403 login_method_disabled for a right and a wrong password alike (which discloses nothing — GET /v1/auth/login-policy publishes the same fact unauthenticated). Read that policy first in scenario 7.
identifierbodystringconditionalyou chooseemailbodystringconditionalyou chooseclientIdbodystringoptionalyou choosepasswordbodystringrequiredyou choosePOST https://api.kerja.team/v1/auth/loginResponse guide — what comes back & what each value means16 fields
200 OK, { data } envelope, Cache-Control: no-store. THREE possible shapes, and the login response itself is the only way to learn which branch applies (there is no pre-login MFA-status endpoint, by anti-enumeration design): (1) tokens — nothing more to prove; (2) a CHALLENGE CHAIN { challengeToken, challenge, remaining } when a device and/or MFA hop is pending, continued at POST /v1/auth/login/challenge/verify; (3) the LEGACY single-hop { mfaRequired, method, phoneHint, mfaToken } when DEVICE_TRUST_ENABLED is off. Only the token branch sets Set-Cookie: refresh_token.
data.accessTokenstringconditionaldata.tokenTypestringconditionalBearer— Send the accessToken as 'Authorization: Bearer <accessToken>'.
data.expiresInnumberconditionaldata.user.idstringconditionaldata.user.emailstringconditionaldata.user.displayNamestringconditionaldata.user.tenantIdstringconditionaldata.challengeTokenstringconditionaldata.challenge.typeenumconditionaldevice_email_code— New browser, email-mode account (LN2) — a code was emailed. Continue at scenario 11.device_phone_otp— New browser, phone-mode account (LN1) — an OTP went out on the account's primary channel.mfa_phone_otp— An enrolled phone-OTP second factor (LE3) — the OTP is sent as the challenge is issued. A recognised device never skips this hop.mfa_totp— An enrolled authenticator app — nothing is sent; the code comes from the app, so no resend applies.
data.challenge.hintstringconditionaldata.challenge.expiresInnumberconditionaldata.remainingnumberconditionaldata.methodenumconditionalphone_otp— Send/resend the OTP with POST /v1/auth/mfa/otp/request, then verify. phone_otp is the method advertised when both are confirmed.totp— Read the code from the authenticator app — there is nothing to send, so skip the otp/request step.
data.phoneHintstringconditionaldata.mfaRequiredbooleanconditionaltrue— MFA challenge pending; proceed to /v1/auth/mfa/verify carrying mfaToken. No access token or refresh cookie issued yet.
data.mfaTokenstringconditional