Authentication
Endpoints for JWT login/logout, password reset, two-factor verification, first-run setup, the password policy, invitation accept, login captcha, SSO login, and retrieving the current session. Everything under /auth/ is public (no auth required). GET /me requires an authenticated session.
Endpoints for JWT login/logout, password reset, two-factor verification, first-run setup, the password policy, invitation accept, login captcha, SSO login, and retrieving the current session. Everything under /auth/ is public (no auth required). GET /me requires an authenticated session.
Get Token
/auth/token
Parameters
| Name | Type | Description |
|---|---|---|
| username required | string | User account username |
| password required | string | User account password |
| captcha_token optional | string | A solved captcha. Required only when the site's login captcha covers the login flow (see `GET /auth/captcha`). For the built-in `cap` provider this is the token from `POST /auth/captcha/redeem`; for Turnstile or reCAPTCHA it is the widget's response token. |
{"username": "admin", "password": "password"}
{"data": {"access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600, "user": {"username": "admin", "fullname": "Site Admin", "email": "[email protected]", "avatar_url": null, "super_admin": true, "access": {"api": {"super": true}, "site": {"login": true}}, "content_editor": "", "demo_mode": {"enabled": false, "writable": [], "reset_interval": 30, "seconds_until_reset": null}}}}
Response Codes
When the account has 2FA enabled, the password is checked but no tokens are issued. The response is still 200, with a short-lived challenge token (valid for 5 minutes) to send to POST /auth/2fa/verify along with the current code:
{"data": {"requires_2fa": true, "challenge_token": "eyJ...", "expires_in": 300, "token_type": "Challenge"}}
Refresh Token
/auth/refresh
Parameters
| Name | Type | Description |
|---|---|---|
| refresh_token required | string | A valid refresh token obtained from the token endpoint |
{"refresh_token": "eyJ..."}
{"data": {"access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600, "user": {"username": "admin", "fullname": "Site Admin", "email": "[email protected]", "avatar_url": null, "super_admin": true, "access": {"api": {"super": true}, "site": {"login": true}}, "content_editor": "", "demo_mode": {"enabled": false, "writable": [], "reset_interval": 30, "seconds_until_reset": null}}}}
Response Codes
Revoke Token
/auth/revoke
Parameters
| Name | Type | Description |
|---|---|---|
| refresh_token required | string | The refresh token to invalidate (body field). |
{"refresh_token": "eyJ..."}
Response Codes
Verify 2FA
/auth/2fa/verify
Parameters
| Name | Type | Description |
|---|---|---|
| challenge_token required | string | The short-lived challenge token returned from `/auth/token`. |
| code required | string | The 6-digit TOTP code from the user's authenticator app. |
{"challenge_token": "eyJ...", "code": "123456"}
{"data": {"access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600, "user": {"username": "admin", "fullname": "Site Admin", "email": "[email protected]", "avatar_url": null, "super_admin": true, "access": {"api": {"super": true}, "site": {"login": true}}, "content_editor": "", "demo_mode": {"enabled": false, "writable": [], "reset_interval": 30, "seconds_until_reset": null}}}}
Response Codes
Forgot Password
/auth/forgot-password
Parameters
| Name | Type | Description |
|---|---|---|
| email required | string | Email address of the account to reset. |
| admin_base_url optional | string | Origin + base path of the calling Admin2 client (e.g. `https://my-site.example/admin`). Used to construct the reset link in the email. Must be an `http`/`https` URL whose origin is the site's own or is listed in the API's `cors.origins`; otherwise it is ignored. Falls back to the `Referer` / `Origin` headers (same origin check), then Grav's own root URL. |
| captcha_token optional | string | A solved captcha. Required only when the site's login captcha covers this flow (see `GET /auth/captcha`). |
{"email": "[email protected]", "admin_base_url": "https://my-site.example/admin"}
{"data": {"message": "If an account exists for that email, a reset link has been sent."}}
Response Codes
Reset Password
/auth/reset-password
Parameters
| Name | Type | Description |
|---|---|---|
| username required | string | Username of the account being reset. |
| token required | string | The reset token from the email link. |
| password required | string | The new password. Must meet the site's password policy. |
{"username": "admin", "token": "abc123...", "password": "N3w-Pa$$w0rd"}
{"data": {"message": "Password reset successfully."}}
Response Codes
Setup Status
/auth/setup
{"data": {"setup_required": true, "password_policy": {"regex": "", "min_length": 8, "rules": [{"id": "length", "label": "At least 8 characters", "pattern": ".{8,}"}]}}}
Response Codes
Create Initial User
/auth/setup
Parameters
| Name | Type | Description |
|---|---|---|
| username required | string | Username (3–64 chars). It may not start with a period or contain `..`, and may not contain `\ / ? * : ; { }` or a line break. |
| password required | string | Password. Must match `system.pwd_regex`, or be at least 8 characters when no regex is set (see `GET /auth/password-policy`). |
| email required | string | A valid email address. |
| fullname optional | string | Display name for the account (defaults to the username). |
| title optional | string | User title (defaults to "Administrator"). |
| captcha_token optional | string | A solved captcha. Required only when the site's login captcha covers the setup flow (see `GET /auth/captcha`). |
{"username": "admin", "password": "a-good-secret", "email": "[email protected]", "fullname": "Site Admin"}
{"data": {"access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600, "user": {"username": "admin", "fullname": "Site Admin", "email": "[email protected]", "avatar_url": null, "super_admin": true, "access": {"api": {"super": true}, "site": {"login": true}}, "content_editor": "", "demo_mode": {"enabled": false, "writable": [], "reset_interval": 30, "seconds_until_reset": null}}}}
Response Codes
Get Current User
/me
{"data": {"username": "admin", "fullname": "Site Admin", "email": "[email protected]", "avatar_url": "/user/accounts/avatars/admin.png", "super_admin": true, "access": {"api": {"access": true, "super": true}, "site": {"login": true}}, "content_editor": "", "demo_mode": {"enabled": false, "writable": [], "reset_interval": 30, "seconds_until_reset": null}, "grav_version": "2.0.0-beta.1", "admin_version": "3.0.0-beta.1"}}
Response Codes
The response includes:
access— the fully resolved permission map (inherited + direct), not the raw YAML. Use this instead of re-deriving permissions client-side.demo_mode: whether the account is a demo account (access.api.demo). For a demo account,writablelists the paths it may still write andseconds_until_resetcounts down to the next demo reset; a normal account getswritable: []andseconds_until_reset: null.reset_interval(minutes) is always present.grav_version— value ofGRAV_VERSIONon the server.admin_version— version of the enabled admin plugin (checksadmin2first, thenadmin), read from itsblueprints.yaml.nullif neither is enabled.content_editor— the user's preferred content editor (empty string if unset).
Password Policy
/auth/password-policy
{"data": {"regex": "(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,}", "min_length": 8, "rules": [{"id": "length", "label": "At least 8 characters", "pattern": ".{8,}"}, {"id": "digit", "label": "At least one number", "pattern": "\\d"}, {"id": "lowercase", "label": "At least one lowercase letter", "pattern": "[a-z]"}, {"id": "uppercase", "label": "At least one uppercase letter", "pattern": "[A-Z]"}]}}
Response Codes
With no system.pwd_regex set, the response is:
{"data": {"regex": "", "min_length": 8, "rules": [{"id": "length", "label": "At least 8 characters", "pattern": ".{8,}"}]}}
Validate Invitation
/auth/invite/{token}
Parameters
| Name | Type | Description |
|---|---|---|
| token required | string | The invitation token from the emailed invite link. |
{"data": {"valid": true, "expired": false, "email": "[email protected]", "fullname": "Jane Doe"}}
Response Codes
Accept Invitation
/auth/invite/{token}
Parameters
| Name | Type | Description |
|---|---|---|
| token required | string | The invitation token from the emailed invite link (path parameter). |
| username required | string | Username (3–64 chars). It may not start with a period or contain `..`, and may not contain `\ / ? * : ; { }` or a line break. |
| password required | string | Password. Must match `system.pwd_regex`, or be at least 8 characters when no regex is set. |
| fullname optional | string | Display name (defaults to the name on the invitation). |
| title optional | string | User title. |
{"username": "jane", "password": "Pa$$w0rd!", "fullname": "Jane Doe"}
{"data": {"access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600, "user": {"username": "jane", "fullname": "Jane Doe", "email": "[email protected]", "avatar_url": null, "super_admin": false, "access": {"api": {"access": true, "pages": {"read": true}}}, "content_editor": "", "demo_mode": {"enabled": false, "writable": [], "reset_interval": 30, "seconds_until_reset": null}}}}
Response Codes
Captcha Config
/auth/captcha
{"data": {"enabled": true, "provider": "cap", "mode": "invisible", "token_field": "captcha_token", "flows": {"login": true, "forgot_password": true, "setup": false}, "endpoint": "https://example.com/api/v1/auth/captcha/"}}
Response Codes
Captcha Challenge
/auth/captcha/challenge
{"challenge": {"c": 30, "s": 32, "d": 4}, "token": "72d323130ee3541275ca2dc1aa9607b4ca61095a08", "expires": 1787332679923}
Response Codes
Captcha Redeem
/auth/captcha/redeem
Parameters
| Name | Type | Description |
|---|---|---|
| token required | string | The challenge token from `POST /auth/captcha/challenge`. |
| solutions required | array | One integer nonce per sub-challenge, in order. |
{"token": "72d323130ee3541275ca2dc1aa9607b4ca61095a08", "solutions": [1043, 88, 5121]}
{"success": true, "token": "e734e7c49c34bb0d:d43a6be90ca08cf9f91171a5320e0a", "expires": 1787333298530}
Response Codes
SSO Providers
/auth/sso/providers
{"data": {"providers": [{"id": "github", "label": "GitHub", "icon": "fa-github", "plugin": "login-oauth2"}]}}
Response Codes
Start SSO Login
/auth/sso/{provider}/start
` instead (default `sso_unavailable`). This route keeps its session cookie so the callback can read the state back. Public (no auth required).
Parameters
| Name | Type | Description |
|---|---|---|
| provider required | string | Provider id from `GET /auth/sso/providers` (path parameter). |
| returnTo optional | string | In-app path to land on after login (query parameter). Only a root-relative path is kept; absolute URLs, `//host` and control characters are dropped. |
Response Codes
SSO Callback
/auth/sso/{provider}/callback
` (plus `&returnTo=` when one was given), where the admin trades it with `POST /auth/sso/exchange`. On failure the browser goes to `/login?sso_error=` with `sso_failed`, `sso_forbidden` (no API access or account disabled), or a provider-specific code. Public (no auth required).
Parameters
| Name | Type | Description |
|---|---|---|
| provider required | string | Provider id (path parameter). |
Response Codes
SSO Exchange
/auth/sso/exchange
Parameters
| Name | Type | Description |
|---|---|---|
| code required | string | Hex code from the `oauth-callback` redirect. |
{"code": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"}
{"data": {"access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600, "user": {"username": "jane", "fullname": "Jane Doe", "email": "[email protected]", "avatar_url": null, "super_admin": false, "access": {"api": {"access": true}}, "content_editor": "", "demo_mode": {"enabled": false, "writable": [], "reset_interval": 30, "seconds_until_reset": null}}}}