{
"publicUrl": "https://auth.example.com",
"listen": ":8081",
"redis": {
"url": "redis://redis:6379/0",
"passwordEnv": "REDIS_PASSWORD",
"prefix": "cladone:iam:v1:",
"authPrefix": "cladone:auth:v1:"
},
"tenants": [{
"id": "company",
"clientId": "gate-one",
"clientSecretEnv": "MYCOMPANY_IAM_CLIENT_SECRET",
"redirectUris": ["https://login.example.com/auth/callback"],
"postLogoutRedirectUris": ["https://app.example.com/"],
"upstreams": {
"google": {
"issuer": "https://accounts.google.com",
"clientIdEnv": "GOOGLE_CLIENT_ID",
"clientSecretEnv": "GOOGLE_CLIENT_SECRET",
"hostedDomain": "example.com"
},
"microsoft": {
"issuer": "https://login.microsoftonline.com/common/v2.0",
"clientIdEnv": "MICROSOFT_CLIENT_ID",
"clientSecretEnv": "MICROSOFT_CLIENT_SECRET",
"optional": true
}
},
"defaultRoles": ["user"],
"allowedDomains": ["example.com"],
"mappings": [
{ "claim": "groups", "values": ["admins"], "roles": ["admin"] }
],
"rules": [
{
"when": [{ "claim": "email", "match": "domain", "values": ["example.com"] }],
"then": {
"roles": ["employee"],
"copyClaims": [
{ "from": "email", "to": "email_domain", "transforms": ["keep:@:-1", "lower"] }
]
}
},
{
"when": [{ "claim": "groups", "match": "glob", "values": ["team-*"] }],
"then": {
"rolesFrom": [
{ "claim": "groups", "transforms": ["keep:-:-1", "lower"] }
]
}
}
]
}]
}Embedded IAM
This example runs the optional embedded IAM cladone-auth as the single
OIDC issuer for Gate One. cladone-auth federates one or more upstream
IdPs (Google, Microsoft Entra, Auth0, any OIDC issuer — each keyed under
upstreams) and normalizes their claims into the roles/scopes/claims
Gate One enforces.
Embedded IAM configuration
upstreams is a map keyed by IdP id; each key is the value of the
idp authorize parameter and the label on the built-in IdP chooser. An
optional upstream is skipped (not shown, not validated) until its
credentials exist, so you can ship it ahead of the secrets.
mappings translates IdP-specific claims into the common model (the
legacy groupRoles block is still accepted for the groups claim).
rules is the richer when/then policy — see below.
Gate One configuration
{
"authProviders": [{
"id": "company",
"type": "oidc",
"issuer": "https://auth.example.com/t/company",
"clientId": "gate-one",
"clientSecretEnv": "MYCOMPANY_OIDC_SECRET",
"loginBaseUrl": "https://login.example.com",
"cookieDomain": "example.com",
"roleClaims": ["roles"],
"sessionKeyEnv": "MYCOMPANY_SESSION_KEY",
"sessionStore": {
"type": "redis",
"url": "redis://redis:6379/0",
"passwordEnv": "REDIS_PASSWORD",
"prefix": "cladone:auth:v1:"
}
}],
"defaultAuthProvider": "company",
"routes": [{
"host": "app.example.com", "path": "/*", "frontend": true,
"backend": "http://app:8080", "auth": { "provider": "company" }
}]
}The Redis store is here because this example is also the multi-instance form; a single IAM instance can use its memory store, and a single Gate One instance can use the default bbolt store.
The tenant’s postLogoutRedirectUris lists where a logout may return.
List every application origin (here app.example.com); an empty list
falls back to redirectUris, which covers only the login host, so a
logout from an application host would be refused. See
Logout and revocation.
Dynamic rights (rules)
Federated users are created at first login with no rights (JIT). Two additive mechanisms grant rights:
rules— evaluated on every login against the incoming token’s claims; the result is never persisted on the user. This is how you give a group of users (by email, domain, subject or IdP group) the right roles/scopes/claims automatically.cladone-auth user set— a persisted per-user exception (below).
A rule reads the token (when) and writes the user (then):
{
"when": [
{ "claim": "email", "match": "domain", "values": ["example.com"] },
{ "claim": "groups", "values": ["support"] }
],
"then": {
"roles": ["employee"],
"scopes": ["read:things"],
"claims": { "department": "support" },
"rolesFrom": [{ "claim": "groups", "transforms": ["keep:-:-1", "lower"] }],
"copyClaims": [{ "from": "email", "to": "email_domain",
"transforms": ["keep:@:-1", "lower"] }]
}
}when— the test, evaluated against the incoming token. Each condition names aclaimand either matchesvalues(match:exact(default),prefix,suffix,domain,glob) or just requires the claim to be present. Several conditions are combined with AND.then.roles/scopes/claims— constant grants: applied whenever the test passes, regardless of which value matched.then.rolesFrom— derived roles: each value of the claim becomes a role.then.copyClaims— derived claims: copy an incoming claim into the issued token, optionally renamed (to) and reshaped.
transforms is an ordered pipeline applied to every value: lower,
upper, prefix:<p>, suffix:<p>, replace:<a>:<b>, and
keep:<sep>:<from>[:<to>] — keep the fields from..to (negative counts
from the end) joined by <sep>; a single index keeps one field. So
keep:@:-1 is the email domain, keep:@:0 the local part, keep:@:0:-2
drops the last field (a@b@c → a@b), and keep:-:-1 turns
team-support into support. Reserved claims (iss, sub, aud,
roles, scope, email, …) can never be written.
Before the first login (pre-provisioning)
A federated user has no IAM record until the first login (JIT). To
give rights to someone who has never signed in, put the condition in a
rule: it is evaluated at each login, so the rights are already there
the first time — without creating a user in advance.
By email
when.claim: "email" accepts one value or a list. With match: "exact"
(the default), the list is effectively a group of emails:
"rules": [{
"when": [{
"claim": "email",
"match": "exact",
"values": ["alice@example.com", "bob@example.com"]
}],
"then": { "roles": ["support"], "scopes": ["read:things"] }
}]When Alice or Bob signs in for the first time, the IAM creates the user
and issues the support role and the read:things scope.
For a whole domain or team, use domain, suffix or glob:
"rules": [
{ "when": [{ "claim": "email", "match": "domain",
"values": ["example.com"] }],
"then": { "roles": ["employee"] } },
{ "when": [{ "claim": "email", "match": "glob",
"values": ["*@team.example.com"] }],
"then": { "roles": ["contractor"] } }
]exact is case-sensitive; domain, prefix, suffix and glob are
case-insensitive. Lowercase the addresses in the configuration (or use
glob) if the IdP does not guarantee the case.
By upstream group (recommended)
The cleanest "group → role" is the group managed by the IdP (Google
Workspace, Entra, Keycloak…): the groups claim is mapped at login.
"mappings": [
{ "claim": "groups", "values": ["support"], "roles": ["support"] },
{ "claim": "groups", "values": ["finance"], "roles": ["finance"] }
]or with the richer rule engine:
"rules": [{
"when": [{ "claim": "groups", "values": ["support"] }],
"then": { "roles": ["support"], "claims": { "department": "support" } }
}]Membership is maintained in the IdP: removing the user from the group removes the role at the next login.
What is evaluated at login
first login
-> JIT user (issuer, subject, email)
-> defaultRoles
-> mappings / legacy groupRoles (upstream claims)
-> rules (email, domain, groups, sub, custom claims)
-> persisted grants (user set; the user must already exist)| Approach | Named group? | Lifecycle |
|---|---|---|
| the list is the group | static; edit the config, reload |
| no | static |
IdP group ( | yes, managed in the IdP | automatic at the next login |
| no (admin only) | static, case-insensitive email list |
| no (persisted exception) | requires the user to exist |
AdminEmails is the built-in email list that grants admin to those
addresses; it is the only email-to-role shortcut outside the rules.
A rule result is never persisted on the user: it is recomputed at
each login. Removing an address from the list (or the user from the IdP
group) removes the role at the next login, but the JIT account remains
with any persisted grants. To remove the account itself, use
cladone-auth user delete once the user has signed in at least once.
user set needs an existing user (issuer/sub), so it cannot
pre-provision a federated identity. For a local account, create it
explicitly with its rights — it is a local identity, not the federated
one:
cladone-auth user create -config embedded-iam.json -tenant company \
-kind local -email alice@example.com -password '...' \
-roles supportThe email claim must be present in the upstream token: the default
scopes include email. If the IdP does not expose it, use groups
or sub as the rule condition instead.
Users and grants
The rules above cover the common case. For a one-off exception, assign roles/scopes/claims to a specific user (persisted):
# inspect the JIT user (issuer/subject come from the IdP token)
cladone-auth user list -config embedded-iam.json -tenant company
cladone-auth user show -config embedded-iam.json -tenant company \
-issuer https://accounts.google.com -sub 'google-sub-1'
# assign common rights
cladone-auth user set -config embedded-iam.json -tenant company \
-issuer https://accounts.google.com -sub 'google-sub-1' \
-roles support -scopes read:things \
-claims '{"department":"support"}'Local users use the same commands with -kind local and a password. The
same roles/scopes/claims model applies to both.
If the IdP subject changes unexpectedly, relink the same IAM user
with -new-issuer/-new-sub instead of deleting it; its grants are
preserved.
Run it
The packaged demo runs Gate One, cladone-auth, a local OIDC provider and Redis (Docker Compose):
cd gate-one-src/deploy/local/embedded-iam
BIN_DIR=~/gate-one-bin ./run.shThe script prints the URLs and stops with ./run.sh down. See [deploy/local/embedded-iam](https://github.com/clad-one/gate-one/tree/main/deploy/local/embedded-iam) for the stack and its README.
To run it by hand instead:
# embedded IAM
export REDIS_PASSWORD=...
export MYCOMPANY_IAM_CLIENT_SECRET=...
export GOOGLE_CLIENT_ID=... GOOGLE_CLIENT_SECRET=...
cladone-auth -config embedded-iam.json
# gate one
export MYCOMPANY_OIDC_SECRET=...
export MYCOMPANY_SESSION_KEY="$(openssl rand -base64 32)"
gate-one -check -config routes.json
gate-one -config routes.jsonWhat to observe
the IAM’s IdP chooser, then Google login;
the IAM session cookie on
auth.example.com;roleClaims: ["roles"]and the mappedadminrole in the Gate One policy;cladone-auth user showafter first login, then a grant update that appears on the next login;a global logout revoking
sidandsub.
Note |
|