ServicesAuth

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:

QueryResult
docs4-auth@example.com1 (exact match on email)
DOCS4-AUTH@EXAMPLE.COM1 (exact match is case-insensitive)
docs41 (matches as a term)
docs4*1 (prefix search)
docs4-auth0 (reads as "docs4 and not auth")
example.com0

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>/impersonate

The 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

On this page