Single-instance identity server for your app. Single Go binary. Replaces Keycloak. Every user is a GUID. LDAP/AD is just another identity provider.
Single Go binary with embedded admin UI. Runs as a Docker container. No external dependencies except an optional AD/LDAP server.
- Users are GUIDs — username, email, display name are just attributes
- LDAP/AD is a provider — maps LDAP usernames to GUIDs, authenticates via bind
- Identity mappings — any provider (LDAP, future SAML/OAuth) maps external IDs to user GUIDs
- Issues signed JWTs with GUID as
sub, plus roles, permissions, and AD groups - Exposes JWKS endpoint so any app can validate tokens
- Impersonation — admins can generate tokens as any user (audited)
- Embedded admin UI — manage everything from the browser, no separate frontend
- BoltDB storage (single file, no database service needed)
- Recommended: run behind nginx for TLS termination
- Go (stdlib
net/httpfor HTTP) - Go
embedpackage for bundling the admin UI - BoltDB via
go.etcd.io/bbolt(pure Go, embedded key-value store) github.com/go-ldap/ldap/v3— LDAP authenticationgithub.com/golang-jwt/jwt/v5— JWT signing/verificationgithub.com/jcmturner/gokrb5/v8— Kerberos/SPNEGOgolang.org/x/crypto/bcrypt— local password hashing- Frontend: Preact + Tailwind CSS (compiled to static files, embedded in binary)
Every user in SimpleAuth is identified by a UUID. That's it. The GUID never changes regardless of username changes, email changes, or provider changes.
LDAP/AD is just one type of provider. A provider:
- Maps an external identifier to a user GUID
- May or may not support authentication (LDAP does)
The link between external identifiers and user GUIDs:
(provider, external_id) -> user_guid- LDAP provider:
(ldap, "kalahmad") -> abc-123
All collapse to the same GUID.
Roles and permissions are global to the SimpleAuth instance. A user has one set of roles and one set of permissions. There is a special built-in role SimpleAuthAdmin — users with this role get full admin access to the SimpleAuth API (equivalent to using the ADMIN_KEY).
Default roles can be configured and are automatically assigned to new users on first login.
{
"sub": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"name": "Khalefa Ahmad",
"email": "kalahmad@corp.local",
"roles": ["admin", "moderator"],
"permissions": ["can_delete_messages", "can_ban_users"],
"groups": ["Domain Users", "IT Department"],
"iss": "simpleauth",
"iat": 1741500000,
"exp": 1741528800
}Impersonated tokens include extra claims:
{
"sub": "a1b2c3d4-...",
"impersonated": true,
"impersonated_by": "d4c3b2a1-...",
"...": "..."
}| Field | Source |
|---|---|
sub |
User GUID (UUID) |
name |
LDAP displayName attr or local user display name |
email |
LDAP mail attr or local user email |
roles |
Global roles for this user |
permissions |
Global permissions for this user |
groups |
Pulled from AD memberOf attribute during LDAP auth |
impersonated |
true if token was issued via impersonation |
impersonated_by |
GUID of the admin who impersonated |
Signed with RS256. RSA-2048 key pair auto-generated on first start, saved to data directory.
| Method | Path | Description |
|---|---|---|
POST |
/api/auth/login |
Login (username + password) |
POST |
/api/auth/refresh |
Refresh access token |
GET |
/api/auth/userinfo |
Get user info from current token |
GET |
/api/auth/negotiate |
Kerberos/SPNEGO login (Windows SSO) |
POST |
/api/auth/impersonate |
Impersonate a user (admin only) |
GET |
/login |
Hosted login page (redirect-based flow) |
GET |
/.well-known/jwks.json |
Public keys for token validation |
GET |
/health |
Health check |
Request:
{
"username": "khalefa",
"password": "secret"
}Response:
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"expires_in": 28800,
"token_type": "Bearer"
}Auth flow:
- Treat username as LDAP username (default field:
sAMAccountName) - If LDAP configured, try LDAP bind with username + password
- If LDAP bind succeeds, pull display name, email, groups from AD
- If LDAP bind fails or not configured, try local user password (bcrypt)
- On success, load roles/permissions, issue JWT with GUID as
sub - On failure, 401
Request (requires admin key or user with can_impersonate permission):
{
"target_guid": "a1b2c3d4-5678-90ab-cdef-1234567890ab"
}Response: same as login, but JWT includes impersonated: true and impersonated_by claims. Shorter TTL (1h default).
Redirect-based login flow for apps that don't want to build their own login form.
Redirect to SimpleAuth:
https://auth.corp.local/login?redirect_uri=https://chat.corp.local/callback
Flow:
- App redirects user to SimpleAuth's
/loginpage withredirect_uri - SimpleAuth shows the login form
- User enters credentials
- SimpleAuth authenticates (same flow as
/api/auth/login) - On success, redirects back to the app:
https://chat.corp.local/callback#access_token=eyJ...&refresh_token=eyJ...&expires_in=28800&token_type=Bearer - On failure, shows error on the login page, user can retry
Security:
redirect_urimust match one of the instance's configured allowed redirect URIs (AUTH_REDIRECT_URIS)- If
redirect_uridoesn't match, reject with error - Tokens passed in URL fragment (
#) not query params (?) so they don't hit server logs
This means apps have two choices for login:
- API-based — build your own login form, call
POST /api/auth/loginfrom your backend - Redirect-based — redirect to SimpleAuth's hosted login page, get tokens back via redirect
Kerberos/SPNEGO flow (transparent Windows SSO):
- Browser sends
Authorization: Negotiate <base64 token> - Server validates Kerberos ticket using keytab
- Extracts username from ticket
- Resolves to user GUID (auto-creates if new)
- Looks up user in LDAP for groups/display name
- Issues JWT with GUID as
sub
If no Negotiate header, responds with 401 + WWW-Authenticate: Negotiate
Requires:
AUTH_KRB5_KEYTABenv var pointing to a keytab fileAUTH_KRB5_REALMenv var (e.g.,CORP.LOCAL)- Service registered as SPN in AD
All admin endpoints require either:
Authorization: Bearer <ADMIN_KEY>header (the bootstrap admin key from env), or- A valid JWT for a user with the
SimpleAuthAdminrole
Multiple LDAP/AD directories can be configured. Each gets a unique provider ID (e.g., corp, partner). Identity mappings reference them as ldap:corp, ldap:partner, etc.
| Method | Path | Description |
|---|---|---|
GET |
/api/admin/ldap |
List all LDAP providers |
GET |
/api/admin/ldap/:provider_id |
Get single LDAP provider config |
POST |
/api/admin/ldap |
Add a new LDAP provider |
PUT |
/api/admin/ldap/:provider_id |
Update LDAP provider config |
DELETE |
/api/admin/ldap/:provider_id |
Remove LDAP provider |
POST |
/api/admin/ldap/:provider_id/test |
Test LDAP provider connection |
GET |
/api/admin/ldap/export |
Export all LDAP provider configs as JSON |
POST |
/api/admin/ldap/import |
Import LDAP provider configs from JSON |
POST |
/api/admin/ldap/auto-discover |
Auto-configure LDAP from domain + service account |
POST |
/api/admin/ldap/setup-kerberos |
One-time Kerberos/SPNEGO setup using privileged creds |
POST /api/admin/ldap body:
{
"provider_id": "corp",
"name": "Corporate AD",
"url": "ldap://dc1.corp.local:389",
"base_dn": "DC=corp,DC=local",
"bind_dn": "CN=svc-auth,OU=Service Accounts,DC=corp,DC=local",
"bind_password": "service-password",
"user_filter": "(sAMAccountName={{username}})",
"use_tls": false,
"skip_tls_verify": false,
"display_name_attr": "displayName",
"email_attr": "mail",
"groups_attr": "memberOf"
}GET /api/admin/ldap/export response:
{
"ldap_providers": [
{
"provider_id": "corp",
"name": "Corporate AD",
"url": "ldap://dc1.corp.local:389",
"base_dn": "DC=corp,DC=local",
"bind_dn": "CN=svc-auth,OU=Service Accounts,DC=corp,DC=local",
"bind_password": "service-password",
"user_filter": "(sAMAccountName={{username}})",
"use_tls": false,
"skip_tls_verify": false,
"display_name_attr": "displayName",
"email_attr": "mail",
"groups_attr": "memberOf",
"priority": 0
}
]
}POST /api/admin/ldap/import — accepts the same JSON format. Upserts providers (creates new, updates existing by provider_id).
Note: Export/import covers LDAP connection config only. Kerberos keytab is a file managed at the instance level (AUTH_KRB5_KEYTAB env var) — Kerberos/SPNEGO won't work on a new instance until the keytab file is also deployed there separately.
Auto-configure an LDAP provider from just a domain name and service account credentials. No need to manually figure out DCs, base DN, or ports.
Request:
{
"domain": "corp.local",
"bind_dn": "CN=svc-auth,OU=Service Accounts,DC=corp,DC=local",
"bind_password": "service-password",
"provider_id": "corp"
}provider_id is optional — defaults to the domain name with dots replaced by dashes (e.g., corp-local).
Auto-discovery flow:
- DNS SRV lookup for
_ldap._tcp.corp.local-> get DC hostnames + ports - Connect to the first reachable DC
- Query RootDSE -> extract
defaultNamingContext(base DN), supported controls, forest info - Set sensible defaults:
user_filter=(sAMAccountName={{username}}),display_name_attr=displayName,email_attr=mail,groups_attr=memberOf - Test bind with provided credentials to verify connectivity
- Return the fully populated LDAP provider config (not yet saved)
Response:
{
"provider_id": "corp",
"name": "corp.local (auto-discovered)",
"url": "ldap://dc1.corp.local:389",
"base_dn": "DC=corp,DC=local",
"bind_dn": "CN=svc-auth,OU=Service Accounts,DC=corp,DC=local",
"bind_password": "service-password",
"user_filter": "(sAMAccountName={{username}})",
"use_tls": false,
"display_name_attr": "displayName",
"email_attr": "mail",
"groups_attr": "memberOf",
"discovered_dcs": ["dc1.corp.local:389", "dc2.corp.local:389"],
"saved": false
}The response includes "saved": false — the caller can review and then POST /api/admin/ldap to save it, or pass "save": true in the request to auto-save.
One-time Kerberos/SPNEGO setup. Uses privileged AD credentials to create the service principal and generate a keytab — then wipes the privileged credentials from memory.
Request:
{
"domain": "corp.local",
"domain_admin_dn": "CN=Administrator,CN=Users,DC=corp,DC=local",
"domain_admin_password": "admin-password",
"service_hostname": "auth.corp.local"
}Setup flow:
- Connect to AD using the privileged domain admin credentials
- Create (or find existing) service account for SimpleAuth
- Register SPN
HTTP/auth.corp.localon the service account - Generate keytab for the SPN
- Save keytab to
{DATA_DIR}/krb5.keytab - Set
AUTH_KRB5_KEYTABandAUTH_KRB5_REALMinternally (runtime config) - Wipe domain admin credentials from memory — they are never stored
- Test: attempt a Kerberos handshake to verify the keytab works
Response:
{
"status": "ok",
"realm": "CORP.LOCAL",
"spn": "HTTP/auth.corp.local",
"keytab_path": "/data/krb5.keytab",
"message": "Kerberos configured. Domain admin credentials have been wiped."
}Security notes:
- Domain admin credentials exist only for the duration of this API call
- They are never written to disk, never logged, never stored in the database
- After setup, only the keytab file is retained — it contains only the service account key, not admin credentials
- This endpoint should only be called once during initial setup
- If the keytab needs to be regenerated, call this endpoint again
Auth flow with multiple LDAP providers:
- Login request comes in -> resolve to user GUID
- Find all
ldap:*mappings for that GUID - Try authenticating against each mapped LDAP provider until one succeeds
- If no GUID yet (new user) -> try each LDAP provider in order until one succeeds, then auto-create user + mapping
| Method | Path | Description |
|---|---|---|
GET |
/api/admin/users |
List all users |
GET |
/api/admin/users/:guid |
Get single user |
POST |
/api/admin/users |
Create local user (auto-generates GUID) |
PUT |
/api/admin/users/:guid |
Update user (display name, email) |
DELETE |
/api/admin/users/:guid |
Delete user |
PUT |
/api/admin/users/:guid/password |
Set local password |
PUT |
/api/admin/users/:guid/disabled |
Enable/disable user |
POST /api/admin/users body:
{
"username": "admin",
"password": "changeme",
"display_name": "Admin User",
"email": "admin@company.com"
}Response includes the generated GUID.
| Method | Path | Description |
|---|---|---|
GET |
/api/admin/users/:guid/mappings |
List all mappings for a user |
PUT |
/api/admin/users/:guid/mappings |
Add/update a mapping |
DELETE |
/api/admin/users/:guid/mappings/:provider/:external_id |
Remove a mapping |
GET |
/api/admin/mappings/resolve |
Resolve an external ID to a GUID |
PUT /api/admin/users/:guid/mappings body:
{
"provider": "ldap:corp",
"external_id": "kalahmad"
}GET /api/admin/mappings/resolve?provider=ldap:corp&external_id=kalahmad -> returns GUID.
Roles and permissions are global to the instance. A user has one set of roles and one set of permissions across the entire SimpleAuth instance.
| Method | Path | Description |
|---|---|---|
GET |
/api/admin/users/:guid/roles |
Get user's roles |
PUT |
/api/admin/users/:guid/roles |
Set user's roles |
GET |
/api/admin/users/:guid/permissions |
Get user's permissions |
PUT |
/api/admin/users/:guid/permissions |
Set user's permissions |
GET |
/api/admin/defaults/roles |
Get default roles for new users |
PUT |
/api/admin/defaults/roles |
Set default roles for new users |
GET |
/api/admin/role-permissions |
Get role-to-permissions mapping |
PUT |
/api/admin/role-permissions |
Set role-to-permissions mapping |
PUT /api/admin/users/:guid/roles body:
["admin", "moderator"]PUT /api/admin/users/:guid/permissions body:
["can_delete_messages", "can_ban_users"]Default roles are automatically assigned to users on first login.
| Method | Path | Description |
|---|---|---|
POST |
/api/admin/users/merge |
Merge two users into a new one |
POST |
/api/admin/users/:guid/unmerge |
Undo a merge (restore original user) |
POST /api/admin/users/merge body:
{
"source_guids": ["guid-A", "guid-B"],
"display_name": "Khalefa Ahmad",
"email": "kalahmad@corp.local"
}Response:
{
"merged_guid": "guid-C",
"sources": ["guid-A", "guid-B"]
}Merge flow:
- Create new user
guid-Cwith provided display name/email - Move all identity mappings from
guid-Aandguid-B->guid-C - Merge roles and permissions (union of both) ->
guid-C - Mark
guid-Aandguid-Basmerged_into = guid-C - All future JWTs use
guid-Cassub - Any API call or login that resolves to
guid-Aorguid-Btransparently followsmerged_into-> returnsguid-C's data
Un-merge flow (POST /api/admin/users/:guid/unmerge):
- Restore original user record (clears
merged_into) - Move back identity mappings that originally belonged to this user
- The user becomes active again with its original GUID
| Method | Path | Description |
|---|---|---|
GET |
/api/admin/backup |
Download a consistent snapshot of the entire DB |
POST |
/api/admin/restore |
Upload a DB snapshot to replace current data |
GET /api/admin/backup — streams the BoltDB file as a binary download. Uses BoltDB's built-in consistent read snapshot, so it works while the server is running with zero downtime. Returns Content-Disposition: attachment; filename="auth-backup-2026-03-09.db".
POST /api/admin/restore — accepts a BoltDB file upload. Validates the file, swaps the current DB, and reloads. This is a destructive operation — the current data is replaced entirely.
For simple backup, you can also just copy {DATA_DIR}/auth.db while the server is stopped.
BoltDB database at {DATA_DIR}/auth.db. Single file, memory-mapped, pure Go.
Optimized for orgs up to 10K users. All lookups are key-based — no SQL, no query engine, just fast reads from a B+ tree.
config # Key-value config
key -> value (string)
e.g. "default_roles" -> '["user"]'
ldap_providers # LDAP provider configs
{provider_id} -> JSON {
provider_id, name, url, base_dn, bind_dn, bind_password,
user_filter, use_tls, skip_tls_verify,
display_name_attr, email_attr, groups_attr,
priority, created_at
}
users # Users by GUID
{guid} -> JSON {
guid, password_hash, display_name, email,
disabled, merged_into, created_at
}
identity_mappings # External ID -> user GUID
{provider}:{external_id} -> {guid}
e.g. "ldap:corp:kalahmad" -> "a1b2c3d4-..."
user_roles # Global roles
{guid} -> JSON ["admin", "moderator"]
user_permissions # Global permissions
{guid} -> JSON ["can_delete_messages", "can_ban_users"]
refresh_tokens # Refresh token tracking (rotation + family revocation)
{token_id} -> JSON {
token_id, family_id, user_guid,
used, expires_at, created_at
}
audit_log # Append-only security event log
{timestamp}:{event_id} -> JSON {
id, timestamp, event, actor, ip, data
}
For lookups that go the "wrong direction" (e.g., find all mappings for a GUID):
idx_mappings_by_guid # GUID -> list of mappings
{guid} -> JSON [{"provider":"ldap:corp","external_id":"kalahmad"}, ...]
These are maintained on write — when a mapping is created, both identity_mappings and idx_mappings_by_guid are updated in the same BoltDB transaction (atomic).
RSA-2048 key pair stored at:
{DATA_DIR}/private.pem{DATA_DIR}/public.pem
Auto-generated on first start if not present.
Single-page app embedded in the Go binary via go:embed. Served at / (root).
- Dashboard — overview: user count, recent logins
- Users — list/search users, view GUID, edit profile, manage roles/permissions, view all identity mappings
- LDAP Providers — add/configure multiple AD/LDAP directories, test connectivity, set priority
- Identity Mappings — browse/search all mappings across providers
- Impersonation — select a user, generate impersonated token
- Login Page — clean login form that apps can redirect to (supports
redirect_uriquery param)
- Preact + Tailwind CSS
- Built to static files at build time
- Embedded into Go binary via
//go:embed ui/dist/* - Served by the same HTTP server on the same port
- API calls go to
/api/*, UI serves from/
| Variable | Default | Description |
|---|---|---|
AUTH_PORT |
9090 |
Listen port |
AUTH_DATA_DIR |
./data |
Directory for BoltDB file and RSA keys |
AUTH_ADMIN_KEY |
(required) | Bootstrap admin API key for admin endpoints |
AUTH_CLIENT_ID |
(optional) | OIDC client ID for this instance |
AUTH_CLIENT_SECRET |
(optional) | OIDC client secret for this instance |
AUTH_REDIRECT_URIS |
(optional) | Comma-separated list of allowed redirect URIs |
AUTH_JWT_ISSUER |
simpleauth |
JWT iss claim |
AUTH_JWT_ACCESS_TTL |
8h |
Access token lifetime |
AUTH_JWT_REFRESH_TTL |
720h |
Refresh token lifetime (30 days) |
AUTH_IMPERSONATE_TTL |
1h |
Impersonated token lifetime |
AUTH_KRB5_KEYTAB |
(optional) | Path to Kerberos keytab file |
AUTH_KRB5_REALM |
(optional) | Kerberos realm (e.g., CORP.LOCAL) |
AUTH_TLS_CERT |
(optional) | Path to TLS certificate (if not using nginx) |
AUTH_TLS_KEY |
(optional) | Path to TLS private key (if not using nginx) |
AUTH_AUDIT_RETENTION |
90d |
How long to keep audit log entries |
simpleauth/
main.go
go.mod
Dockerfile
.env.example
build.md
ui/ # Frontend source
src/
index.html
app.jsx
pages/
Dashboard.jsx
Users.jsx
LdapProviders.jsx
Mappings.jsx
Impersonate.jsx
Login.jsx
components/
Layout.jsx
Table.jsx
Modal.jsx
tailwind.config.js
package.json
dist/ # Built static files (embedded)
internal/
config/config.go # Bootstrap config from env vars
store/store.go # BoltDB store (all buckets, CRUD)
auth/
jwt.go # RSA key management, JWT sign/verify, JWKS
ldap.go # LDAP bind + attribute fetch
local.go # bcrypt password verification
spnego.go # Kerberos/SPNEGO (optional)
handler/
auth.go # Login, refresh, userinfo, negotiate, impersonate
admin.go # User/mapping/role/permission CRUD
admin_ldap.go # LDAP provider CRUD endpoints
middleware.go # Admin key + SimpleAuthAdmin role validation
ui.go # Serve embedded UI files
FROM node:22-alpine AS ui-build
WORKDIR /ui
COPY ui/package.json ui/package-lock.json ./
RUN npm ci
COPY ui/ .
RUN npm run build
FROM golang:1.24-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
COPY --from=ui-build /ui/dist ./ui/dist
RUN go build -o /simpleauth .
FROM alpine:3.21
RUN apk add --no-cache ca-certificates
COPY --from=build /simpleauth /simpleauth
EXPOSE 9090
VOLUME /data
ENV AUTH_DATA_DIR=/data
ENTRYPOINT ["/simpleauth"]Run behind nginx for TLS termination:
server {
listen 443 ssl;
server_name auth.corp.local;
ssl_certificate /etc/ssl/certs/auth.pem;
ssl_certificate_key /etc/ssl/private/auth.key;
location / {
proxy_pass http://127.0.0.1:9090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}SimpleAuth can also serve TLS directly via AUTH_TLS_CERT and AUTH_TLS_KEY for simpler setups.
Apps validate JWTs by fetching the public key from /.well-known/jwks.json.
Users are always identified by GUID (sub claim).
// Fetch JWKS once at startup
jwks := auth.FetchJWKS("http://simpleauth:9090/.well-known/jwks.json")
// Middleware: validate JWT on every request
e.Use(auth.JWTMiddleware(jwks))
// In handlers: user GUID from JWT
userGUID := auth.GetUserGUID(c) // "a1b2c3d4-5678-..."
// Check roles/permissions
if !auth.HasRole(c, "admin") {
return c.JSON(403, "forbidden")
}// Login
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "khalefa", password: "secret" })
});
const { access_token, refresh_token } = await res.json();
localStorage.setItem("token", access_token);
// API calls — user is identified by GUID in the token
fetch("/api/v1/conversations", {
headers: { Authorization: `Bearer ${access_token}` }
});- Start SimpleAuth with
AUTH_ADMIN_KEY=your-secret - Server auto-generates RSA key pair, creates empty BoltDB
- Open the admin UI at
http://localhost:9090 - Configure LDAP connection, test connectivity
- Create initial admin user, assign
SimpleAuthAdminrole - Set default roles for new users
- Users can now login via
/api/auth/loginwith their usernames - LDAP users are auto-created in BoltDB (with new GUID) on first login with default roles
- User authenticates -> receives
access_token(8h) +refresh_token(30d) - Frontend sends
access_tokenon every API request - When access token expires, frontend calls
POST /api/auth/refreshwith refresh token - Server issues new access token and a new refresh token (rotation)
- Old refresh token is invalidated — each refresh token is one-time use
- If a previously-used refresh token is resubmitted -> revoke the entire token family (likely stolen)
- If refresh token is expired -> user must login again
Refresh tokens are stored in BoltDB keyed by token ID, grouped into families (all tokens from the same login session). This enables detecting replay attacks.
Simple per-IP throttling on login endpoints. In-memory, no external dependencies.
POST /api/auth/login— max 10 attempts per IP per minuteGET /api/auth/negotiate— max 20 attempts per IP per minute- After limit exceeded ->
429 Too Many RequestswithRetry-Afterheader
Implementation: in-memory sliding window counter per IP. Automatically cleans up stale entries. Resets on server restart (intentional — keeps it simple).
Not a replacement for nginx rate limiting or fail2ban, but provides basic protection out of the box.
All security-relevant events are logged to a dedicated BoltDB bucket. Append-only, keyed by timestamp.
| Event | Logged Data |
|---|---|
login_success |
user GUID, provider used, IP |
login_failed |
username attempted, reason, IP |
token_refresh |
user GUID |
impersonation |
admin GUID, target GUID, IP |
user_created |
new GUID, created_by |
user_merged |
source GUIDs, merged GUID, merged_by |
user_unmerged |
restored GUID, unmerged_by |
role_changed |
user GUID, old roles, new roles, changed_by |
permission_changed |
user GUID, old perms, new perms, changed_by |
ldap_provider_added |
provider_id, added_by |
kerberos_setup |
realm, SPN, setup_by |
Each entry:
{
"id": "evt-uuid",
"timestamp": "2026-03-09T14:30:00Z",
"event": "login_success",
"actor": "a1b2c3d4-...",
"ip": "10.0.1.50",
"data": { "provider": "ldap:corp" }
}| Method | Path | Description |
|---|---|---|
GET |
/api/admin/audit |
Query audit log (paginated, filterable) |
Query params: ?event=login_failed&from=2026-03-01&to=2026-03-09&user=guid&limit=100&offset=0
Configurable via AUTH_AUDIT_RETENTION env var (default 90d). A background goroutine prunes old entries daily.