Users, login, and IdP
Create local operators, log in for a JWT, see how roles gate management, and wire an external identity provider. Evaluation stays on SDK tokens.
Virtual environment active, shell at the repository root, process on 127.0.0.1:8080. Flag walkthrough: Checkout rollout. All examples.
01Signing secret and bootstrap
Local login needs FLAGGATOR_JWT_SECRET (at least 32 bytes for HS256). Set it before bootstrap and before starting the process.
source .venv/bin/activate export FLAGGATOR_JWT_SECRET="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" python -m main bootstrap
Bootstrap creates flaggator.db when missing, applies migrations, and prints one opaque management token on stdout once. Save it for the first user and for recovery. Prefer JWT users afterward.
export MGMT=the-printed-management-token
In another terminal, with the same secret exported:
export FLAGGATOR_JWT_SECRET=the-same-secret-as-above python -m main
02Create the first user
Users are global. Creating them needs a writer (or the management token). Roles are a non-empty list from reader, writer, and protected_writer.
curl -s -X POST http://127.0.0.1:8080/v1/users \
-H "Authorization: Bearer $MGMT" \
-H "Content-Type: application/json" \
-d '{
"email": "ops@example.com",
"name": "Ops",
"password": "password1",
"roles": ["reader", "writer", "protected_writer"]
}'
{
"id": 1,
"email": "ops@example.com",
"name": "Ops",
"roles": ["reader", "writer", "protected_writer"],
"created_at": "2026-10-31T15:00:00Z",
"disabled_at": null
}
03Local login
POST /v1/auth/login needs no bearer. Wrong password, unknown email, disabled user, and IdP-only user (no password) share the same 401.
curl -s -X POST http://127.0.0.1:8080/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"ops@example.com","password":"password1"}'
{
"access_token": "eyJ…",
"token_type": "bearer",
"expires_in": 3600
}
export TOKEN='paste-the-access_token-here'
Use TOKEN for management calls. Issuer, audience, and TTL come from FLAGGATOR_JWT_ISSUER, FLAGGATOR_JWT_AUDIENCE, and FLAGGATOR_JWT_TTL_SECONDS (default 3600).
04Manage with the JWT
curl -s -X POST http://127.0.0.1:8080/v1/projects \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"key":"shop","name":"Shop"}'
curl -s http://127.0.0.1:8080/v1/users \
-H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://127.0.0.1:8080/v1/evaluate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"project_key":"shop","environment_key":"prod","flag_key":"x","context":{"subject":"a","attributes":{}},"default":false}'
05Roles in practice
| Role | Grants |
|---|---|
| reader | GET projects, environments, flags, targeting, audit, users. |
| writer | All reader rights, plus create/mutate on non-protected resources. |
| protected_writer | Add-on with writer for protected environments. Alone it grants nothing. |
Create a reader-only user and log in as them:
curl -s -X POST http://127.0.0.1:8080/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"reader@example.com","name":"Reader","password":"password1","roles":["reader"]}'
curl -s -X POST http://127.0.0.1:8080/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"reader@example.com","password":"password1"}'
export READER='paste-reader-access_token'
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://127.0.0.1:8080/v1/projects \
-H "Authorization: Bearer $READER" \
-H "Content-Type: application/json" \
-d '{"key":"other","name":"Other"}'
Create a protected environment with the ops JWT, then try to clear protected with a writer who lacks protected_writer:
curl -s -X POST http://127.0.0.1:8080/v1/projects/shop/environments \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"key":"prod","name":"Production","protected":true}'
curl -s -X POST http://127.0.0.1:8080/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"writer@example.com","name":"Writer","password":"password1","roles":["reader","writer"]}'
curl -s -X POST http://127.0.0.1:8080/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"writer@example.com","password":"password1"}'
export WRITER='paste-writer-access_token'
curl -s -X PATCH http://127.0.0.1:8080/v1/projects/shop/environments/prod \
-H "Authorization: Bearer $WRITER" \
-H "Content-Type: application/json" \
-d '{"protected":false}'
The ops JWT (writer + protected_writer) succeeds:
curl -s -X PATCH http://127.0.0.1:8080/v1/projects/shop/environments/prod \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"protected":false}'
06IdP-only user (no local password)
Omit password when the person will only authenticate through an external IdP. Local login then returns the same 401 as a bad password.
curl -s -X POST http://127.0.0.1:8080/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"idp@example.com","name":"IdP Ops","roles":["reader","writer"]}'
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://127.0.0.1:8080/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"idp@example.com","password":"does-not-matter"}'
07External IdP via JWT
One IdP per process. Before start, point flaggator at the provider’s JWKS and at the issuer / audience you expect on tokens:
export FLAGGATOR_JWT_JWKS_URL='https://your-idp.example/.well-known/jwks.json' export FLAGGATOR_JWT_ISSUER='https://your-idp.example/' export FLAGGATOR_JWT_AUDIENCE='flaggator' # keep FLAGGATOR_JWT_SECRET for local HS256 login as well python -m main
What happens on each request:
- Bearer alg is RS* or ES* → verify signature against JWKS, then iss, aud, exp.
- Load the local user by decimal sub (local user id) or by normalized email claim.
- Roles come from that local row. The IdP is not trusted to invent protected_writer.
- No matching enabled user → 401, same shape as an unknown opaque token.
After the IdP issues a token for idp@example.com, call the API with that token as the bearer (no /v1/auth/login exchange):
export IDP_JWT='paste-idp-access-token' curl -s http://127.0.0.1:8080/v1/projects \ -H "Authorization: Bearer $IDP_JWT"
08Disable a user
curl -s -X PATCH http://127.0.0.1:8080/v1/users/2 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"disabled":true}'
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://127.0.0.1:8080/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"reader@example.com","password":"password1"}'