Authentication
Endpoints for JWT login/logout, password reset, two-factor verification, first-run setup, and retrieving the current session. Token/refresh/revoke/2FA/setup/forgot/reset endpoints are public (no auth required). GET /me requires an authenticated session.
Endpoints for JWT login/logout, password reset, two-factor verification, first-run setup, and retrieving the current session. Token/refresh/revoke/2FA/setup/forgot/reset endpoints are public (no auth required). GET /me requires an authenticated session.
Get Token
POST
/auth/token
Generate JWT access and refresh tokens. This is a public endpoint that does not require prior authentication.
Parameters
| Name | Type | Description |
|---|---|---|
| username required | string | User account username |
| password required | string | User account password |
JSON
{"username": "admin", "password": "password"}
JSON
{"data": {"access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600}}
Response Codes
200
Tokens generated
401
Invalid credentials
Refresh Token
POST
/auth/refresh
Refresh an expired access token using a valid refresh token. This is a public endpoint that does not require prior authentication.
Parameters
| Name | Type | Description |
|---|---|---|
| refresh_token required | string | A valid refresh token obtained from the token endpoint |
JSON
{"refresh_token": "eyJ..."}
Response Codes
200
New tokens generated
401
Invalid or expired refresh token
Revoke Token
POST
/auth/revoke
Revoke a refresh token (explicit logout). Best-effort decodes the token to record the user for the `onApiUserLogout` event, then revokes it unconditionally. Always returns 204, even if the token was already invalid — revoke is idempotent.
Parameters
| Name | Type | Description |
|---|---|---|
| refresh_token required | string | The refresh token to invalidate (body field). |
JSON
{"refresh_token": "eyJ..."}
Response Codes
204
Token revoked (or already invalid).
400
Missing refresh_token field.
Verify 2FA
POST
/auth/2fa/verify
Exchange a 2FA challenge token plus a TOTP code for a full token pair. Returned when `/auth/token` responds with `requires_2fa: true`. The challenge token is single-use and expires in 5 minutes. On success, fires `onApiUserLogin` with `method: 2fa`.
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. |
JSON
{"challenge_token": "eyJ...", "code": "123456"}
JSON
{"data": {"access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600}}
Response Codes
200
Verification succeeded; token pair issued.
400
Missing challenge_token or code.
401
Invalid/expired challenge token, or invalid 2FA code.
403
Account disabled, or 2FA support unavailable on the server.
429
Too many failed attempts; rate limited.
Forgot Password
POST
/auth/forgot-password
Request a password reset email. Always returns a neutral success message regardless of whether the email matches an account — prevents account enumeration. Rate-limited per-user via the Login plugin's `pw_resets` bucket. Requires the Email and Login plugins configured.
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://example.com/admin`). Used to construct the reset link in the email. Must be an `http`/`https` URL. Falls back to the `Referer` / `Origin` headers, then Grav's own root URL. |
JSON
{"email": "[email protected]", "admin_base_url": "https://example.com/admin"}
JSON
{"data": {"message": "If an account exists for that email, a reset link has been sent."}}
Response Codes
200
Request accepted (neutral response regardless of match).
400
Missing email field.
429
Rate limit exceeded for this user.
Reset Password
POST
/auth/reset-password
Complete a password reset using the token from the reset email. All failures return the same vague error message to prevent token probing from distinguishing bad user / wrong token / expired token. Rate-limited per-username. Fires `onApiPasswordReset` on success.
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. |
JSON
{"username": "admin", "token": "abc123...", "password": "new-secret"}
JSON
{"data": {"message": "Password reset successfully."}}
Response Codes
200
Password updated.
400
Missing required fields, or invalid/expired reset link.
429
Too many attempts; rate limited.
Setup Status
GET
/auth/setup
Check whether the instance requires first-run setup. Returns `setup_required: true` only when `user/accounts/` is empty. Admin2 polls this on load to decide between showing the setup wizard or the login screen. Public (no auth required).
JSON
{"data": {"setup_required": true}}
Response Codes
200
Status returned successfully.
Create Initial User
POST
/auth/setup
One-time first-run setup — creates the initial super-admin account on a fresh Grav 2.0 install. Active only while `user/accounts/` is empty; 409 thereafter. Grants `api.super` (not `admin.super`) by default so the account has API authority without implying classic-admin authority. Fires `onApiUserCreated` and `onApiSetupComplete`, then issues a login token pair so the client can skip straight to the dashboard. Public (no auth required), rate-limited per IP.
Parameters
| Name | Type | Description |
|---|---|---|
| username required | string | Username (3–64 chars; letters, numbers, hyphens, underscores). |
| password required | string | Password (minimum 8 characters). |
| email required | string | A valid email address. |
| fullname optional | string | Display name for the account. |
| title optional | string | User title (defaults to "Administrator"). |
JSON
{"username": "admin", "password": "a-good-secret", "email": "[email protected]", "fullname": "Site Admin"}
JSON
{"data": {"access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600}}
Response Codes
200
Account created; token pair issued.
400
Validation failed (username format, invalid email, password too short, missing fields).
409
Setup has already been completed.
429
Too many setup attempts from this IP.
Get Current User
GET
/me
Return the authenticated user's profile and resolved permissions. Admin2 calls this on every load to bootstrap the UI with the current identity, access map, running Grav core version, and active admin plugin version.
JSON
{"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": "", "grav_version": "2.0.0-beta.1", "admin_version": "3.0.0-beta.1"}}
Response Codes
200
Profile returned.
401
Not authenticated.
403
User lacks `api.access`.
The response includes:
access— the fully resolved permission map (inherited + direct), not the raw YAML. Use this instead of re-deriving permissions client-side.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).