Integrate with tenka.auth
Add passwordless, phone-approved login to your application. Your app asks Tenka to authenticate a user; the user approves on their phone (Face ID / fingerprint / PIN) with number matching; your app receives a signed, verifiable result. Your app never handles passwords or passkeys.
How it works Prerequisites Base URL & auth Integration flow Endpoint reference Verifying the result Full example Security
How it works
tenka.auth is a push-approval authenticator (the Duo/Okta-Verify model). It is a factor, not a password store. The exchange is always the same three steps:
- Your app calls
POST /auth/requestwith the user's email and your API key. - Tenka pushes a request to the user's registered device(s); the user approves and matches a number.
- Your app polls the request until it resolves, then verifies the returned EdDSA-signed JWT against Tenka's public JWKS.
Because the result is a signed token bound to your application, a forged "approved" message is worthless — you verify it cryptographically, offline.
Prerequisites
- An Application + API key. An org admin creates an Application in the
admin console; that mints an
API key(shown once) and anapplication id(this is the token audience — see below). - The user exists in your org and is granted access to that Application (admin console → Users). Self-enrolled users have no app access until an admin grants it.
- The user has a registered device. A request for a user with no device returns
409 user_has_no_registered_device.
Base URL & authentication
Base URL:
https://mfa.tenka.cloud/api/v1
Every request except the JWKS endpoint requires your Application API key, sent as a bearer token
(x-api-key is also accepted):
Authorization: Bearer ak_your_application_api_key
The API key is a server-side secret. Call this API from your backend only — never from a browser or mobile client, and never commit it to source control.
Integration flow
- User initiates login in your app (they identify themselves, e.g. by email).
- Your backend calls
POST /auth/requestwith that email. - If
number_matchingis true, displaynumber_codeto the user — their phone will ask them to pick the matching number. - Poll
GET /auth/request/:id(or the returnedpoll_url) every ~2s untilstatusis no longerpending, or untilexpires_atpasses. - On
approved, verifyresult_tokenagainst the JWKS (checking issuer, audience, expiry). Then create your app's own session for the user. - On
denied/expired/failed, refuse the login.
Endpoint reference
POST /auth/request
Create an authentication request and push it to the user.
Request body (JSON):
| Field | Required | Description |
|---|---|---|
username | yes | The user's email address. |
reason | no | Why (shown to the user), e.g. "VPN login". |
location | no | Where from (shown to the user), e.g. "Stockholm, SE". |
requesting_device | no | What is asking, e.g. "VPN Client" (max 200 chars). |
201 Created response:
{
"request_id": "req_...",
"status": "pending",
"number_matching": true,
"number_code": "042", // DISPLAY this to the user (null if disabled)
"expires_at": 1737000000000, // epoch ms
"poll_url": "https://mfa.tenka.cloud/api/v1/auth/request/req_..."
}
Errors:
| Status | error | Meaning |
|---|---|---|
400 | username_required | No email supplied. |
401 | missing_api_key / invalid_api_key | Bad or absent key. |
403 | application_disabled | The Application is disabled. |
403 | source_not_allowed | The Application has a source-address allow-list in enforce mode and the call came from another address. Treat as a denial; the organization admin has been alerted and can approve a new address in the admin console. |
404 | unknown_user | No such active user in your org, or the user has no access grant to this app (the two are deliberately indistinguishable). |
409 | user_has_no_registered_device | User must enroll a device first. |
429 | too_many_requests | Push throttle: after 3 denied or unanswered requests in a row (15 min window) the user is paused for 15 minutes; also at most 10 requests per 10 minutes. Honour the Retry-After header and treat it as a denial. |
GET /auth/request/:id
Poll a request. Requires the same API key; you can only read your own Application's requests.
{ "request_id": "req_...", "status": "pending" }
status is one of pending, approved, denied,
expired, cancelled, failed. When approved, two
extra fields appear:
{
"request_id": "req_...",
"status": "approved",
"result_token": "eyJhbGciOiJFZERTQS...", // EdDSA JWT — verify it
"jwks_url": "https://mfa.tenka.cloud/api/v1/.well-known/jwks.json"
}
When denied or failed, a reason field is included.
POST /auth/request/:id/cancel
Cancel a pending request (e.g. the user navigated away). Returns { "ok": true, "status": "cancelled" }.
GET /.well-known/jwks.json
Public keys for verifying result tokens. No API key required; cache up to 5 minutes.
{ "keys": [ { "kty": "OKP", "crv": "Ed25519", "x": "...", "kid": "...", "alg": "EdDSA" } ] }
Verifying the result token
The result_token is an EdDSA (Ed25519) JWT. Always verify it — treat
it, not the bare status, as the proof of authentication. Check:
- Signature against the JWKS above.
- Issuer (
iss) equalshttps://mfa.tenka.cloud. - Audience (
aud) equals yourapplication id. - Expiry (
exp) — tokens live 2 minutes and carry a single-usejti.
Claims:
| Claim | Meaning |
|---|---|
result | Always "approved" (a token is only minted on approval). |
sub | Stable Tenka user id. |
email / name | The authenticated user. |
org_id / org_name | The user's organization. |
app_name | Your Application's name. |
amr | Methods used, e.g. ["webauthn"]. |
auth_time | When the user approved (epoch seconds). |
aud / iss / exp / iat / jti | Standard JWT claims (verify these). |
Full example
1 — Start the request:
curl -s https://mfa.tenka.cloud/api/v1/auth/request \
-H "Authorization: Bearer $TENKA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"username":"jon@example.com","reason":"VPN login","location":"Stockholm, SE"}'
2 — Poll until resolved:
curl -s https://mfa.tenka.cloud/api/v1/auth/request/req_abc123 \
-H "Authorization: Bearer $TENKA_API_KEY"
3 — Verify the token (Node.js, using jose):
import { jwtVerify, createRemoteJWKSet } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://mfa.tenka.cloud/api/v1/.well-known/jwks.json'));
const { payload } = await jwtVerify(resultToken, JWKS, {
issuer: 'https://mfa.tenka.cloud',
audience: MY_APPLICATION_ID, // the Application id from the admin console
});
// payload.sub, payload.email — the verified user. Now log them in.
Security notes
- Keep the API key server-side. It can create login requests for any user in your org — treat it like a database password. Load it from the environment; rotate it if exposed.
- Trust the token, not the status. Only the verified
result_tokenproves authentication; verify issuer, audience and expiry every time. - Number matching resists prompt-bombing. Display
number_codein your app; the user must select it on their phone, so a blind "approve" can't succeed. - One request, one login. Tokens are single-use (
jti) and expire in 2 minutes. Don't cache or replay them. - Fail closed. If the request expires or the service is unreachable, deny the login.
Questions or need an Application provisioned? Contact your tenka.auth administrator.
