Authentication
Every request carries a bearer token in the Authorization header:
Authorization: Bearer <token>For your backend, the token is a session token for one staff member. You get it by signing that person in with their email, password and a code from their authenticator app. The person must supply the code each time they sign in.
Sign in
Section titled “Sign in”Signing in takes two calls.
1. Send the password
Section titled “1. Send the password”POST /api/v1/auth/sign-in takes email and
password:
jq -n '{email: env.ELE_EMAIL, password: env.ELE_PASSWORD}' | curl -s "$ELE_API_BASE/api/v1/auth/sign-in" \ -H 'Content-Type: application/json' \ --data @-jq reads the credentials from the environment and encodes them as JSON, so a
password with quotes or backslashes still works. --data @- reads the body
from standard input, so the password does not appear in the process list.
A wrong email or password returns 401 with the error type
invalid_credentials. Otherwise the response is 200, and its status says
what happens next:
status |
Meaning | What to do |
|---|---|---|
mfa_required |
The password was right. Ele needs a code from the person’s authenticator app. | Send the code with the challenge_token (step 2). |
authenticated |
Signed in. The response has a session token. |
Use the token. |
totp_setup_required |
An organization the person belongs to requires an authenticator app, and they have not set one up. | See Setting up an authenticator app. |
webauthn_required |
The account has no authenticator app, and this device holds a passkey for it. | Not possible from a server. See Passkeys. |
authenticated comes straight back when the account has no authenticator app,
so the password alone signs it in. Do not run staff accounts like that: give
every staff member an authenticator app.
2. Send the authenticator code
Section titled “2. Send the authenticator code”POST /api/v1/auth/mfa takes the
challenge_token and the person’s current code:
curl -s "$ELE_API_BASE/api/v1/auth/mfa" \ -H 'Content-Type: application/json' \ -d "{\"challenge_token\": \"$CHALLENGE_TOKEN\", \"code\": \"$ELE_TOTP_CODE\"}"The response is 200, and its status is one of:
status |
Meaning | What to do |
|---|---|---|
authenticated |
Signed in. The response has a session token and the user. |
Use the token. |
mfa_failed |
The code was wrong, or was already used. | Ask the person for a new code and send it with the same challenge_token. |
challenge_expired |
The challenge token is too old or no longer valid. | Start again from step 1. |
A challenge token lasts 10 minutes. Each authenticator code works only once, so
if the person has just used a code, they must wait for the next one. After too
many wrong codes, a challenge token keeps returning mfa_failed, even for a
right code. Start again from step 1.
The example
Section titled “The example”This program signs a person in and prints the session token:
// Sign-in: sign a staff member in with their password and an authenticator// code, and print the session token.//// export ELE_TOKEN=$(ELE_EMAIL=… ELE_PASSWORD=… ELE_TOTP_CODE=123456 go run ./signin)//// The token is the only thing written to standard output, so a shell can// capture it. Everything else goes to standard error. The program refuses to// print the token to a terminal, where it would stay in the scrollback. In// your own backend, keep the token in your secret store, never in logs.//// Set ELE_DEVICE_ID to the device id from an earlier sign-in to sign in as// that device rather than a new one.package main
import ( "context" "fmt" "log" "os"
"github.com/elephants-labs/ele/developers/examples/internal/ele")
// signInResult covers both the sign-in and the MFA responses. Status says// which of the other fields are set.type signInResult struct { Status string `json:"status"` Token string `json:"token"` ChallengeToken string `json:"challenge_token"`}
func main() { if stat, err := os.Stdout.Stat(); err == nil && stat.Mode()&os.ModeCharDevice != 0 { log.Fatal("refusing to print a session token to a terminal; capture it instead: export ELE_TOKEN=$(go run ./signin)") } ctx := context.Background() c := ele.FromEnv()
body := map[string]string{ "email": os.Getenv("ELE_EMAIL"), "password": os.Getenv("ELE_PASSWORD"), } if id := os.Getenv("ELE_DEVICE_ID"); id != "" { body["device_id"] = id } var res signInResult err := c.Do(ctx, "POST", "/api/v1/auth/sign-in", body, &res) if err != nil { log.Fatal(err) }
if res.Status == "mfa_required" { // The challenge token lasts 10 minutes. On "mfa_failed" you may ask // the person for a new code and send it with the same challenge token. err := c.Do(ctx, "POST", "/api/v1/auth/mfa", map[string]string{ "challenge_token": res.ChallengeToken, "code": os.Getenv("ELE_TOTP_CODE"), }, &res) if err != nil { log.Fatal(err) } }
switch res.Status { case "authenticated": fmt.Println(res.Token) case "mfa_failed": log.Fatal("the authenticator code was wrong or already used; wait for the next code and try again") case "challenge_expired": log.Fatal("the sign-in challenge expired; start again") case "totp_setup_required": log.Fatal("an organization this person belongs to requires an authenticator app, and they have not set one up yet") case "webauthn_required": log.Fatal("this account has no authenticator app and this device holds a passkey, which only works on Ele's own apps; add an authenticator app to the account") default: log.Fatalf("unexpected sign-in status %q", res.Status) }}Run it from the examples directory:
export ELE_TOKEN=$(ELE_TOTP_CODE=123456 go run ./signin)It reads ELE_EMAIL and ELE_PASSWORD from the environment. It refuses to
print the token to a terminal, so capture it as shown.
Setting up an authenticator app
Section titled “Setting up an authenticator app”If sign-in returns totp_setup_required, the person can set up an authenticator
app as part of signing in:
- Call
POST /api/v1/auth/sign-in/totp/setupwith thechallenge_token. It returnsuriandsecret_encoded. - Show the
urito the person, for example as a QR code, so they can add it to their own authenticator app. - Call
POST /api/v1/auth/sign-in/totp/activatewith thechallenge_tokenand acodefrom the app. It answers like the MFA call above.
The secret belongs in the person’s authenticator app and nowhere else. Do not store it, log it or keep it on your server.
A person who is already signed in can add an authenticator app with
POST /api/v1/auth/totp/setup and
POST /api/v1/auth/totp/activate. The
activate call needs both a code from the app and a WhatsApp code that Ele sends
during setup.
Passkeys
Section titled “Passkeys”A passkey is bound to Ele’s own web address. Browsers and phones will only use
it on Ele’s sites and apps, so your server cannot complete a passkey sign-in.
If sign-in returns webauthn_required, add an authenticator app to that
account. When an account has an authenticator app, sign-in always asks for its
code instead of a passkey.
Devices
Section titled “Devices”Each sign-in belongs to a device. If you do not send device_id, Ele records a
new device for every sign-in, and may notify the person about a sign-in from a
new device.
To avoid that, after the first sign-in call
GET /api/v1/devices and keep the id of
the entry with this_device: true. Send it as device_id the next time that
person signs in from your server.
The sign-in example sends ELE_DEVICE_ID as device_id when
you set it.
Keep a person in the loop
Section titled “Keep a person in the loop”Ele expects a person to be present when they sign in and when they approve something. Some operations exist for Ele’s own mobile apps, which keep a credential behind the phone’s lock. Called from a server, they would let that server sign in or approve payments with no person present. Do not use them from a server:
- Device trust. Leave
remember_me,trust_transportandtrust_tokensunset when you sign in. With them, Ele issues a token that lets later sign-ins skip the authenticator code for 30 days. - Device login credentials:
/api/v1/auth/login-credentialand/api/v1/auth/sign-in/device. They sign a person in without a password or a code. - Approval credentials and challenges:
/api/v1/me/approval-credentialand the/api/v1/me/challengesoperations. They let a phone answer step-up for the person, including payment approvals.
Do not register your server as one of the person’s phones by any other route either. Ele’s rule is that a person approves each payment, and these pages ask you to keep it that way.
Using the token
Section titled “Using the token”Send the token on every request:
curl -s "$ELE_API_BASE/api/v1/me" \ -H "Authorization: Bearer $ELE_TOKEN"How long a session lasts
Section titled “How long a session lasts”A session ends at whichever comes first:
- 30 minutes without use. Each authenticated request moves the end to 30 minutes after that request.
- 30 days after sign-in. Use does not extend this limit.
When a session has ended, every request with its token returns 401:
{ "error": { "type": "not_authenticated", "message": "Unauthorized", "request_id": "…" }}A session cannot be extended past 30 days. To continue, sign the person in again, which needs a new code from them. Plan your product so a staff member can sign in again when their session ends.
Sign out
Section titled “Sign out”POST /api/v1/auth/sign-out ends the session
whose token you send:
curl -s -X POST "$ELE_API_BASE/api/v1/auth/sign-out" \ -H "Authorization: Bearer $ELE_TOKEN"It always returns 200 with {"message": "goodbye"}, even when the token has
already expired. Call it when a staff member signs out of your product, and
then delete your copy of the token.
Storing tokens
Section titled “Storing tokens”A session token acts as the person until it ends. Treat it like a password:
- Keep it in a secret store or encrypted storage, never in source code.
- Never write it to logs, error reports or analytics.
- Keep one token per staff member, and never share a token between people.
- Never store a staff member’s password or authenticator secret.