Administration
Operator-facing endpoints for looking after other people's accounts:
finding a user, reading their full identity picture, banning, impersonating,
revoking sessions, and onboarding users in bulk. Everything here requires
a platform admin token - a platform_role of admin in the claims.
Anything less is refused before the handler runs, with
403 Admin access required. Current platform role: user.
These routes live under /admin/auth/*, apart from bulk onboarding which
sits at /api/v1/auth/users/batch.
Finding a user
snug auth admin users --query you@example.com
snug auth admin users --banned true --page-size 50
snug auth admin users --role admin
snug auth admin users --email-verified false{
"users": [
{
"id": "xhMLHYzgKQPSnTSsfcmz",
"email": "docs4-auth@example.com",
"email_verified": true,
"role": "user",
"banned": false,
"two_factor_enabled": false
}
],
"pagination": { "total": 1, "page": 1, "page_size": 25, "has_more": false }
}Search by the whole email address. The q parameter tries an exact
match on email and then on username, and only falls back to full-text search
if neither hits. That fallback runs through the shared search grammar, where
a hyphen is a negation operator, so a partial term containing one quietly
matches nothing. For a user whose address is
docs4-auth@example.com:
| Query | Result |
|---|---|
docs4-auth@example.com | 1 (exact match on email) |
DOCS4-AUTH@EXAMPLE.COM | 1 (exact match is case-insensitive) |
docs4 | 1 (matches as a term) |
docs4* | 1 (prefix search) |
docs4-auth | 0 (reads as "docs4 and not auth") |
example.com | 0 |
An empty page from this endpoint is not evidence that an account does not
exist. When a partial term returns nothing, retry with the full address, or
with a single term plus *.
Reading one user
snug auth admin user --id <user_id>The detail view is a composite, not just the user row - it is the fastest way to answer "why can this person not get in":
{
"user": {
"id": "xhMLHYzgKQPSnTSsfcmz",
"email": "docs4-auth@example.com",
"email_verified": true,
"role": "user",
"banned": false
},
"identities": [],
"org_memberships": [
{ "org_id": "dqxPXsVANGJNbqchQTgS", "org_name": "Docs4 Auth Org", "role": "owner" }
],
"passkey_count": 0,
"api_key_count": 0,
"active_session_count": 2
}identities lists external provider links, so an empty array on a member of
an SSO-enforced organization is the whole explanation for a locked-out user.
Changing an account
snug auth admin user-update --id <user_id> --role admin
snug auth admin user-delete --id <user_id>PATCH /admin/auth/users/{id} accepts display_name, email_verified, and
role, each optional. Setting email_verified to true is the practical
way to unblock an account on a deployment where mail is not being delivered,
since auth's organization endpoints read that stored field rather than the
token claim.
Deletion answers 200 and is not reversible. Afterwards the address behaves
as if it never existed: signing in with it returns
401 invalid_credentials, the same answer a wrong password gets.
Banning
snug auth admin user-ban --id <user_id> --reason "abuse report #123"
snug auth admin user-ban --id <user_id> --reason "temporary" --expires 2026-12-31T23:59:59Z
snug auth admin user-unban --id <user_id>reason is required; omitting expires makes the ban permanent. A banned
user's sign-in fails with the reason included in the message:
{
"status": 403,
"msg": "user banned: docs4 verification of the ban flow",
"error": "user_banned"
}That text is shown to the banned person, so write the reason for their eyes, and keep internal case notes elsewhere. Unbanning restores password sign-in immediately.
Sessions
snug auth admin sessions --user-id <user_id>
snug auth admin session-revoke --id <session_id>The listing carries a user_agent alongside the timestamps, which is what
distinguishes one live session from another:
{
"id": "YhgtQsudFmGrgYgSaXfw",
"user_id": "xhMLHYzgKQPSnTSsfcmz",
"created_at": "2026-08-28T23:02:17.505851Z",
"expires_at": "2026-09-27T23:02:17.505475Z",
"user_agent": "CLI browser login",
"active": true
}Admin revocation answers 200 (the self-service equivalent answers 204)
and takes effect at once, because session state is consulted on every
request.
Revoking sessions does not touch the user's API keys. Verified: with
every session of an account revoked, its access token returned 401 while
one of its API keys still wrote to KV successfully. To cut off a compromised
account completely, revoke the sessions and then revoke or rescope the keys -
or force a password change, which revokes both at once.
Impersonation
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" -d '{}' \
http://localhost:4000/admin/auth/users/<user_id>/impersonateThe response is an ordinary token pair for the target user. Decoding the
access token shows a normal 15-minute user token, sub and azp both set
to the target, platform_role: user, and no marker claim separating it
from one the user obtained themselves.
The limit is on the session behind it, which lives one hour rather than
thirty days (AUTH_SERVICE_IMPERSONATION_SESSION_TTL_SECONDS). Because the
token itself is indistinguishable, treat impersonation as a privileged
action to log on your side, and revoke the session when you are done rather
than letting it idle out.
Bulk onboarding
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"users":[{"email":"a@example.com","password":"..."},
{"email":"b@example.com","password":"..."}]}' \
http://localhost:4000/api/v1/auth/users/batch{ "user_ids": ["uArMwXWvBAKtUuzvGtNd", "uMTUAneeuMDWNLBfXecF"], "count": 2 }Each entry takes the same shape as a signup request. No verification email
is sent and no session is created, so the accounts land unverified and
inert - pair this with user-update --email-verified true, or with an
identity provider, depending on how the users will actually sign in. The
per-request cap is 100 by default
(AUTH_SERVICE_MAX_BATCH_USERS, valid range 1 to 1000).
Reference
- Auth API - every admin endpoint
- Auth service overview - concepts and the other task pages
- auth CONFIG reference - impersonation and batch limits