OAuth 2.0 flow

Authorization request, token exchange, and refresh token — the OAuth 2.0 Authorization Code Flow with PKCE.

Overview

Enbox uses the standard OAuth 2.0 Authorization Code Flow with PKCE (RFC 6749 + RFC 7636). Every application on every device is a separate OAuth2 client with its own client_id.

The flow has three stages:

  1. Authorize — user logs in and approves the requested permissions (consent screen)
  2. Token exchange — client exchanges the authorization code for access + refresh tokens
  3. Refresh — client rotates the refresh token to get a new access token

Prelogin (E2E key derivation)

Before signing in, the client needs the user’s salt_user and kdf_params to derive the password hash locally. The server never sees the plaintext password.

HTTP

Request

GET https://auth.enbox.net/prelogin?email=user@example.com

Query parameters

Parameter Type Required Description
email string The user’s email address

Response

{
  "salt_user": "base64-encoded-16-byte-salt",
  "kdf_params": {
    "alg": "argon2id",
    "memory_kib": 65536,
    "iterations": 3,
    "parallelism": 4,
    "version": "0x13"
  },
  "missing": []
}
Field Type Description
salt_user string Base64-encoded 16-byte Argon2id salt
kdf_params object Argon2id parameters for key derivation
missing string[] Key types the user doesn’t have yet (e.g. ["pgp"])

For non-existent emails, the response contains deterministic fake values (derived from HKDF(server_secret, email)) — indistinguishable from real ones to prevent email enumeration.

The client then:

  1. Derives master_key = Argon2id(password, salt_user, kdf_params) locally
  2. Derives master_pwd_hash = PBKDF2(master_key, password, iter=1, sha256) locally
  3. Submits master_pwd_hash (not the plaintext password) in the sign-in form
  4. If missing contains key types, the client generates them silently in the background and includes them in the sign-in POST

Authorization request

Starts the OAuth 2.0 authorization process. The user is redirected to this URL, logs in, and authorizes the application on the consent screen.

If the user is not logged in, they are redirected to the sign-in page first (with 2FA verification), then back to the consent screen.

HTTP

Request

GET https://auth.enbox.net/authorize
    ?response_type=code
    &client_id=myapp-client-id
    &redirect_uri=https://myapp.com/callback
    &scope=notes.note.view contacts.contact.read
    &state=randomstring
    &code_challenge=g91XPJpIt0PoaQXs2OEImlMdL_LGgH5HLrYxT-Ug5hk
    &code_challenge_method=S256

Query parameters

Parameter Type Required Description
response_type string Must be "code"
client_id string The application’s Client ID
redirect_uri string The callback URL — must exactly match the registered value
scope string Space-separated permission keys (e.g. "notes.note.view contacts.contact.read")
state string Random string for CSRF protection — returned as-is
code_challenge string PKCE Code Challenge — BASE64URL(SHA256(code_verifier)), 43–128 chars
code_challenge_method string Must be "S256"

Required vs optional scopes

Each application declares two lists of scopes:

  • Required — the app cannot function without these. Shown as locked checkboxes on the consent screen.
  • Optional — the app works without them but offers additional functionality. Shown as toggleable checkboxes.

The user can uncheck any optional scope. The granted scope in the token response reflects only the permissions the user approved.

Response

On user approval:

302 Found
Location: https://myapp.com/callback?code=AUTH_CODE&state=randomstring

On user denial:

302 Found
Location: https://myapp.com/callback?error=access_denied&state=randomstring

On invalid parameters:

302 Found
Location: https://myapp.com/callback?error=invalid_scope&error_description=...&state=randomstring

Note: client_id and redirect_uri errors return 400 directly (no redirect) to prevent open-redirect attacks.

Token exchange

Exchanges an authorization code for an access token and a refresh token. The authorization code is single-use and expires after 5 minutes.

HTTP

Request

POST https://auth.enbox.net/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=myapp-client-id
&code=AUTH_CODE
&redirect_uri=https://myapp.com/callback
&code_verifier=randomstringxyz123456789

Body parameters

Parameter Type Required Description
grant_type string Must be "authorization_code"
client_id string The application’s Client ID
code string Authorization code received from /authorize
redirect_uri string Must exactly match the value in the authorization request
code_verifier string Original random string used to generate code_challenge
client_secret string ⚠️ Required for confidential clients only

Response

{
  "access_token": "dQw4w9WgXcQ...",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "refresh-token-xyz",
  "scope": "notes.note.view contacts.contact.read",
  "key_material": {
    "salt_user": "base64",
    "kdf_params": { "alg": "argon2id", "memory_kib": 65536, "iterations": 3, "parallelism": 4, "version": "0x13" },
    "wrapped_account_sym": "base64",
    "wrapped_account_priv": "base64",
    "account_pub": "base64",
    "wrapped_pgp_priv": "base64",
    "pgp_pub": "base64"
  }
}
Field Type Description
access_token string Bearer token for API requests
token_type string Always "Bearer"
expires_in number Access token lifetime in seconds (900 = 15 min)
refresh_token string Long-lived token for obtaining new access tokens
scope string Space-separated granted permissions
key_material object Wrapped encryption keys (first-party clients only)

key_material is only returned on grant_type=authorization_code (first login on device) and only for first-party clients. Third-party clients receive an empty object — they work with encrypted blobs via the API and never see key material.

Error responses

{
  "error": "invalid_grant",
  "error_description": "Invalid or expired authorization code."
}
Error Cause
invalid_request Missing required parameter
invalid_client Unknown/inactive client, or bad client_secret
invalid_grant Invalid/expired code, PKCE failure, redirect_uri mismatch
unsupported_grant_type Grant type is not authorization_code or refresh_token

Refresh token

Use a refresh token to obtain a new access token. The old refresh token is revoked and a new one is issued (rotation).

HTTP

Request

POST https://auth.enbox.net/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&client_id=myapp-client-id
&refresh_token=refresh-token-xyz

Body parameters

Parameter Type Required Description
grant_type string Must be "refresh_token"
client_id string The application’s Client ID
refresh_token string The refresh token received earlier
client_secret string ⚠️ Required for confidential clients only

Response

{
  "access_token": "new-access-token",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "new-refresh-token",
  "scope": "notes.note.view contacts.contact.read"
}

Rotation: The old refresh token is revoked on every refresh. Using a revoked refresh token returns invalid_grant. The old access token is deleted immediately. No key_material is returned on refresh — the client already has the keys from the initial login.

Token TTL summary

Token Storage Lifetime Notes
Authorization code Redis 5 min Single-use (atomic GETDEL)
Access token Redis 15 min Resolved on every API request
Refresh token PostgreSQL 12 months Rotated on every refresh