Keycloak and custom OIDC
This page tells you how to give an install single sign-on. You can use the bundled Keycloak, with or without Active Directory (AD), or your own OpenID Connect (OIDC) provider.
| Option | Use it when | Settings in local-env.yaml |
|---|---|---|
| Password login (default) | You do not need single sign-on. | None |
| Bundled Keycloak | You want the install to run its own provider. You can connect AD or Google Workspace to it. Use Google Workspace to turn Google groups into roles. | keycloak: and oidc: |
| Your own provider | You already have Entra ID, Okta, Google or another provider. | oidc: |
The commands use the default namespace, octostar-main. Replace <context>
with your Kubernetes context, and <domain> with octostar.domain. Add
--context <context> to each kubectl command when the current context is not
the install.
⚠️ Warning — To move an install that already has users, read Move Keycloak or switch provider first.
keycloak.enabled: truewith anoidc:section moves an existing install onto an empty Keycloak.
How login works
- Octostar at
https://home.<domain>sends every login to the provider. The password form is not available there. - The API creates or finds the Timbr user, then applies the role mapping.
- At the first login, a claim gives the username (
usernameClaim). Usernames are permanent, because entity data and workspace permissions store them. - The API stores the provider's issuer and subject with the username, in the
oidc_identity_linkstable. A returning user is found by this link, not by the username claim. So a user renamed at the provider keeps their Octostar user. - A first login adopts an existing Timbr user when its name equals the claim, without case. This works only when no other identity holds that username.
- Fusion logs a provider user in only as the Timbr user that the link names. Fusion
creates no users. So a new user logs in to
https://home.<domain>before Fusion.
With an oidc: section, Fusion's login page goes straight to the provider. Local
users, such as admin, use https://fusion.<domain>/login/?local=1.
Use the bundled Keycloak
keycloak.enabled is false by default.
1. Choose where Keycloak lives
Choose before the first user logs in. A later change moves every identity link (procedure).
keycloak.location | URL | Issuer | What you need |
|---|---|---|---|
hostname (recommended) | https://auth.<domain> | https://auth.<domain>/realms/octostar | A DNS record or a wildcard for auth.<domain>, to where home.<domain> points |
subpath (default) | https://home.<domain>/auth | https://home.<domain>/auth/realms/octostar | Nothing more |
We recommend hostname for production, for security. With subpath, Keycloak
shares the home.<domain> origin with the workspace apps. If you turn on the admin
console there, an app opened in the browser of a logged-in Keycloak admin can act
as that admin. With hostname, Keycloak never shares an origin with the apps.
With hostname, the install makes the ingress and the certificate. On an
octostar.internalDomain install, it also makes the CoreDNS rewrite for the pods.
It does not make the public DNS record. If the record is missing, the installer
prints a warning, and nobody can log in through Keycloak.
2. Set local-env.yaml
keycloak:
enabled: true
location: hostname
oidc:
usernameClaim: "preferred_username"
autoRegister: true
defaultRole: "os_user"
groupsClaim: "groups"
allowAdminRoles: true
roleMapping:
octostar-users: "os_user"
octostar-admins: ["os_user", "os_admin", "admin", "admin_all_workspaces"]
Do not set oidc.issuer, oidc.clientId or oidc.clientSecret. The install sets
the first two and generates the secret into the oidc-client-secret Secret. If you
set an issuer or clientId that differs, or any clientSecret, the install stops
and tells you which key to remove.
⚠️ Warning — Keep the
oidc:section. Without it, Keycloak and its realm run, but nothing logs in through them.
The main keycloak: keys:
| Key | Default | Meaning |
|---|---|---|
replicas | 1 | Keycloak pods |
db.poolSize | 20 | PostgreSQL connections per pod |
clientId | octostar | The client that Fusion and the API use |
groups | octostar-users, octostar-admins | Groups that the install creates. Removing a name does not delete the group. |
adminConsole.enabled | false | Routes /admin/ and /realms/master/ |
adminPassword | empty (generated) | Password of the master realm's admin: 16 or more of A-Z a-z 0-9 . _ ~ - |
localAdmin.username, localAdmin.group, localAdmin.email | generated, octostar-admins, generated | See Local admin |
loginConnectionsPerIp | 0 (no limit) | Login posts in progress per client address |
theme.enabled | true | The Octostar look on the login pages |
config.enabled | true | false skips the keycloak-config Job, for example to finish an update while the Job fails |
local-env.template.yaml lists every key.
3. Run the install or the update
./bin/install.sh --context <context> # a new install
./bin/update-existing.sh --context <context> # an existing install
The command ends with the username of the local admin, and the commands that read its password.
Air-gapped install: the lists of the delivery bundle name the Keycloak and Operator
images, the keycloak-config-cli image and the Operator chart. Set
keycloak.operatorImage, keycloak.image and keycloak.configCliImage only where
the admission mutator does not rewrite the registry. Not verified: we read these
lists, but we did not run an offline install with Keycloak.
4. Check the result
-
Read the issuer. Use
home.<domain>/authin place ofauth.<domain>withsubpath:curl -sk https://auth.<domain>/realms/octostar/.well-known/openid-configuration | jq .issuer -
Read the local admin's username and password:
kubectl -n octostar-main get secret keycloak-octostar-admin -o jsonpath='{.data.username}' | base64 -dkubectl -n octostar-main get secret keycloak-octostar-admin -o jsonpath='{.data.password}' | base64 -d -
Open
https://home.<domain>and log in as the local admin. -
Open
https://fusion.<domain>and log in again through Keycloak.
What the install manages
On every install and update, the keycloak-config Job applies the octostar realm:
- The client, with these redirect URIs:
https://fusion.<domain>/oauth-authorized/oidcandhttps://home.<domain>/api/auth/oidc/callback. - The post-logout redirect URIs:
https://home.<domain>/api/auth/oidc/logged-out,https://home.<domain>/andhttps://fusion.<domain>/login/. - A groups claim, named as
oidc.groupsClaim. It holds group names, not paths. - The groups in
keycloak.groups, and the local admin. - No self-registration, no username editing and no account console.
- Brute-force detection in the
octostarandmasterrealms.
Objects that you make in the admin console stay. But each sync sets back what the Job declares, for example the client's redirect URIs and mappers, and the local admin's group and password.
A logout from Fusion also ends the Keycloak session. Only /realms/octostar/ and
/resources/ are routed by default. /metrics and /health are never routed.
Add users without a directory
Without AD, make each user in Keycloak. The admin console is off by default, so use
kcadm.sh in the pod. A browser cannot use the console through
kubectl port-forward, because the console redirects to the public URL. To use a
browser, set keycloak.adminConsole.enabled: true, and read
the risk first.
kubectl -n octostar-main exec -it keycloak-0 -- bash
# Then, in the pod:
kc() { /opt/keycloak/bin/kcadm.sh "$@" --config /tmp/kc.config; }
KC_CLI_PASSWORD=$KC_BOOTSTRAP_ADMIN_PASSWORD kc config credentials \
--server "http://localhost:8080${KC_HTTP_RELATIVE_PATH:-}" \
--realm master --user "$KC_BOOTSTRAP_ADMIN_USERNAME"
kc create users -r octostar -s username=alice -s enabled=true \
-s email=alice@example.com -s emailVerified=true -s firstName=Alice -s lastName=Doe
kc set-password -r octostar --username alice --new-password '<temporary password>' --temporary
# Add the user to a group.
uid=$(kc get users -r octostar -q exact=true -q username=alice --fields id --format csv --noquotes)
gid=$(kc get groups -r octostar -q exact=true -q search=octostar-users --fields id --format csv --noquotes)
kc update "users/$uid/groups/$gid" -r octostar -s realm=octostar -s userId="$uid" -s groupId="$gid" -n
rm -f /tmp/kc.config
The pod's KC_BOOTSTRAP_ADMIN_* variables come from the keycloak-admin Secret. If
the password in Keycloak differs, the login fails. The username becomes the Timbr username at the first login. Give the temporary
password to the user out of band. Without SMTP, Keycloak cannot send reset emails.
Turn Keycloak off
keycloak.enabled: false uninstalls the Operator and Keycloak. The keycloak role,
database and Secrets stay, so turning it on again finds the same data.
Also change the oidc: section. If it stays as it is, the install stops with
oidc.issuer is required, as a string, when local-env.yaml has an oidc: section.
Choose one:
-
Another provider: set
oidc.issuer,oidc.clientIdandoidc.clientSecret, and replace the links. -
Password login: remove the
oidc:section. The users that the API made have a random Timbr password that nobody knows. So give each of them a password first, in Fusion's SQL Lab, asadmin. Use letters and digits only. Not verified: Fusion's code uses this statement, but we did not run it.ALTER USER `alice` SET PASSWORD = '<random letters and digits>';
Connect Active Directory
Use this section only with the bundled Keycloak.
Keycloak reads AD and never writes to it. Users log in with the AD login
(sAMAccountName) and AD password, and change the password in AD. Keycloak imports
a user at the first login and at each sync, and checks the password against AD at
every login. A user who is disabled or locked in AD cannot log in.
1. Prepare AD
- Make a read-only service account for the bind. Any domain user can read users and groups.
- Make sure that AD serves TLS. AD serves TLS only with a certificate on each
domain controller. Plain
ldap://without StartTLS is refused, because every password crosses this connection. - Get the CA certificate, in PEM format, when Keycloak's JVM does not trust it.
- Make sure that the host name in the connection URL is the name in the certificate.
- Name the AD groups as the
oidc.roleMappingkeys, and put them under one groups DN. - Read AD's lockout policy for the lockout settings.
2. Add the directory
Add the ldap list to the keycloak: section of the
example above:
keycloak:
enabled: true
location: hostname
ldap:
- type: ad
connectionUrl: "ldaps://dc1.example.com:636"
bindDn: "svc-keycloak@example.com"
bindPassword: "<password>"
usersDn: "OU=People,DC=example,DC=com"
groupsDn: "OU=Octostar,OU=Groups,DC=example,DC=com"
caCert: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
| Key | Default | Meaning |
|---|---|---|
type | required | ad. For google, see Connect Google Workspace. |
name | active-directory | Keycloak's provider name: 1 to 32 of a-z 0-9 -. See the warning below. |
enabled | true | false keeps the provider and its users, and blocks their logins. |
connectionUrl | required | ldaps://<host>[:port], or ldap://<host>[:port] with startTls: true |
startTls | false | Use StartTLS on an ldap:// URL. |
bindDn, bindPassword | required | The service account. The password goes into the keycloak-ldap Secret, and a sync applies a change with no Keycloak restart. |
usersDn | required | Where the users are. The search covers the whole subtree. |
userFilter | (objectCategory=person) | Which users to import |
groupsDn | required | Only groups under this DN reach the token. |
groupFilter | empty | Which groups to import |
caCert | empty | The CA of the AD certificate, in PEM format. Restart Keycloak after a change. |
hostAliases | empty | A list of {ip, hostnames}, when the cluster DNS does not resolve the AD host |
masterRealm | false | Adds the connection to the master realm too. AD users have no rights there until an admin gives an AD group a role in the console. |
fullSyncPeriodSeconds | 86400 | Full sync period. -1: no periodic sync. |
changedSyncPeriodSeconds | 3600 | Changed-users sync period. -1: no periodic sync. |
⚠️ Warning — Never change
nameafter users log in. A renamed or removed entry is a removed provider, and Keycloak deletes its users. They come back with new subjects, and the API refuses them. To recover, replace the links: the issuer stays the same, and every subject changes. To stop logins, useenabled: false.
3. Set the lockout settings
Keycloak locks an account after repeated wrong passwords. The lock opens after the wait, with no admin action.
keycloak.bruteForce key | Default | Rule with AD |
|---|---|---|
failureFactor | 3 | Below AD's lockout threshold |
waitIncrementSeconds | 1200 | Above AD's "reset account lockout counter after" time |
maxFailureWaitSeconds | 1200 | waitIncrementSeconds or more |
failureResetSeconds | 43200 | The time with no failure after which the count starts again |
With these rules, wrong passwords sent one at a time cannot lock AD accounts.
Wrong passwords sent at the same time can, because Keycloak counts failures in the
background. To limit this, set keycloak.loginConnectionsPerIp to a small number,
such as 2. nginx then answers 503 above the limit. This needs the client's own
address at nginx, and it does not stop wrong passwords from many addresses. Keep 0
when many users log in at the same time from one address.
4. Apply and check
-
Run
./bin/update-existing.sh --context <context>. -
Log in to
https://home.<domain>as an AD user who is in a mapped group. -
In Fusion's SQL Lab, as
admin, check the user's roles:SELECT role FROM timbr.sys_users_roles WHERE LOWER(username) = '<ad login in lowercase>';
The token then holds:
| Claim | Value |
|---|---|
preferred_username | The AD login, in lowercase. It becomes the Timbr username, so a Timbr user with that name keeps the account. |
upn | The userPrincipalName |
groups | The user's AD groups under groupsDn, by name. Nested groups count. |
email | The AD mail, when the user has one. With none, the API uses the UPN: the install sets oidc.emailClaim: upn unless you set it. |
Keycloak reads a user and their groups from AD again after 10 minutes. So a group change can take 10 minutes, and then applies at the user's next Octostar login.
Connect Google Workspace
Use this section only with the bundled Keycloak. Users log in with their Google
account, and Keycloak reads their Google groups from Google Secure LDAP, so the groups
reach oidc.roleMapping. This needs two parts: the directory, which reads users and
groups, and a Google identity provider, which does the login. The install makes the
directory. You make the identity provider once, with kcadm.sh, and later updates keep
it.
When to use it. Use this section when Google groups must grant Octostar roles. Use Google, in Use your own OIDC provider when you accept roles granted by hand: Google tokens carry no groups. Secure LDAP is only in some Workspace editions, such as Business Plus, Enterprise and Education. Business Starter and Business Standard have no Apps > LDAP, so step 1 cannot be done there. See Google's list.
1. Prepare Google
- In Google Admin, Apps > LDAP, add an LDAP client. Turn on Read user information and Read group information. Turn off Verify user credentials, and give it no access credentials. Download its certificate and key, and set the client to On. Google can take 24 hours to apply a change.
- In Google Admin, Apps > Google Workspace > Groups for Business > Sharing
settings, let only admins make groups. A group named as a
roleMappingkey grants its roles. - In a Google Cloud project of your organization, make an OAuth client of type Web
application, with an OAuth consent screen of audience Internal. Add this
authorized redirect URI:
subpath:https://home.<domain>/auth/realms/octostar/broker/google/endpointhostname:https://auth.<domain>/realms/octostar/broker/google/endpoint
2. Add the Google directory
Add an entry of type google to the ldap list of the keycloak: section, and run
./bin/update-existing.sh --context <context>. Leave out the oidc: section until step
4, if the install has users: see
Move password users to a provider.
keycloak:
enabled: true
ldap:
- type: google
baseDn: "dc=example,dc=com"
groupFilter: "(|(cn=os-admin)(cn=os-developer))"
clientCert: |
-----BEGIN CERTIFICATE-----
...
clientKey: |
-----BEGIN PRIVATE KEY-----
...
| Key | Default | Meaning |
|---|---|---|
baseDn | required | The Workspace domain, such as dc=example,dc=com |
groupFilter | required | The groups that reach the token. List only groups that admins own. |
clientCert, clientKey | required | The LDAP client's certificate and key, from step 1 |
name | google-workspace | Keycloak's provider name. Never change it after users log in: see the warning in Connect Active Directory. |
local-env.template.yaml lists the other keys. Keycloak reads the directory through
the keycloak-google-ldap proxy, which presents the certificate to Google.
On a new install, nothing more is needed: the first Google login finds the directory user by email and imports it. If the install has users already, import the directory users before you move their links: see Import the directory users.
3. Make the Google identity provider
In the Keycloak pod, with kc from
Add users without a directory. The read keeps the
client secret off the screen and out of the shell history.
read -rsp 'Google client secret: ' secret; echo
printf '{"alias":"google","providerId":"google","displayName":"Google","enabled":true,"trustEmail":true,"storeToken":false,"firstBrokerLoginFlowAlias":"link-existing-user","config":{"clientId":"%s","clientSecret":"%s","hostedDomain":"%s","defaultScope":"openid profile email","syncMode":"IMPORT"}}' \
"<client id>.apps.googleusercontent.com" "$secret" "example.com" \
| kc create identity-provider/instances -r octostar -f -
unset secret
| Setting | Why |
|---|---|
hostedDomain | Google offers only accounts of your Workspace domain. |
firstBrokerLoginFlowAlias: link-existing-user | A Google login takes the directory user with the same email. A Google account that the directory does not have is refused. The install makes this flow. |
trustEmail: true | The API needs a verified email at a first login. |
syncMode: IMPORT | The name and email come from Google at the first login only. |
If the provider exists already, the create answers Conflict. Send the same JSON to
kc update identity-provider/instances/google -r octostar --merge -f - instead.
4. Turn on the Google login
- Add the
oidc:section, withgroupsClaimand aroleMappingwhose keys are the Google group names, and run the update. - Open
https://home.<domain>. The Keycloak page shows a Google button. - Log in with a Google account that is in a mapped group, and check its roles as in Connect Active Directory.
The token then holds:
| Claim | Value |
|---|---|
preferred_username | The full email. It becomes the Timbr username at the first login. A Timbr user with another name needs a pre-link. |
groups | The user's Google groups that groupFilter matches, by name. Only direct members. |
email | The Google email |
If the Google login was on before you added the directory, a Keycloak user that a Google login made has no groups. Delete that user in Keycloak, and delete its link, so that the next login takes the directory user.
Map groups to roles
The API applies oidc.roleMapping at each Octostar login. It grants the mapped
roles, and it revokes a role that the mapping granted before when no group maps to
it now. Roles granted by hand stay. Fusion's own group mapping stays off, and a
Fusion login changes no roles.
- Give
os_userto every user. It gives ontology access. Fusion'seditor,analystandviewergive no ontology access. defaultRoleapplies when no group maps. It is required, and it cannot be an admin-class role.- Admin-class roles are
admin,os_adminand every*_all_workspacesrole.roleMappingcan grant them only withallowAdminRoles: true. roleMappingneedsgroupsClaim. The install refusesroleMappingwithout it. Without both keys, every user gets the default role.- Keys are exact. A key must equal the value in the groups claim.
- A role granted directly in Keycloak does not reach Timbr. Use a group.
See Users and roles for what each role gives.
Take care with group names. The claim holds names, not paths, so a group with a
mapped name grants its roles at any level of the group tree. Let only admins make
groups. With AD, limit the groups with groupsDn.
Recovery access
| Problem | Account | Where the credentials are | How to log in |
|---|---|---|---|
| AD or Google is down, Keycloak is up | Local admin of the octostar realm | keycloak-octostar-admin Secret | https://home.<domain> |
| Keycloak or your own provider is down | Fusion's admin | timbrAdminPassword | https://fusion.<domain>/login/?local=1 |
| You must administer Keycloak | admin of the master realm | keycloak-admin Secret | kcadm.sh in the pod, or the console with keycloak.adminConsole.enabled |
With your own provider, Octostar has no local login. While the provider is down,
nobody can log in to https://home.<domain>.
An Octostar login never uses admin, platform or os_system. These names, and
the API's service account, are reserved. The API refuses a provider user with one of
these names.
To clear a Keycloak lock, use kc from
Add users without a directory. For another master
realm account, use -r master:
uid=$(kc get users -r octostar -q exact=true -q username=<username> --fields id --format csv --noquotes)
kc delete "attack-detection/brute-force/users/$uid" -r octostar
kc logs in as the master realm's admin, so it cannot clear a lock on that account.
Wait for the lock to end (keycloak.bruteForce.waitIncrementSeconds, by default 20
minutes), or log in with the config client:
client=$(kubectl -n octostar-main get secret keycloak-config-client -o jsonpath='{.data.client-id}' | base64 -d)
kubectl -n octostar-main get secret keycloak-config-client -o jsonpath='{.data.client-secret}' | base64 -d |
kubectl -n octostar-main exec -i keycloak-0 -- bash -c '
IFS= read -r KC_CLI_CLIENT_SECRET; export KC_CLI_CLIENT_SECRET
kc() { /opt/keycloak/bin/kcadm.sh "$@" --config /tmp/kc.config; }
kc config credentials --server "http://localhost:8080${KC_HTTP_RELATIVE_PATH:-}" --realm master --client "$1"
uid=$(kc get users -r master -q exact=true -q "username=$KC_BOOTSTRAP_ADMIN_USERNAME" --fields id --format csv --noquotes)
kc delete "attack-detection/brute-force/users/$uid" -r master
rm -f /tmp/kc.config' _ "$client"
The keycloak-config-client Secret holds the secret of the Job's own master realm
client. This client has the admin role, so its secret is a standing Keycloak admin
credential. Protect it, and the backup bucket, as you protect keycloak-admin.
Backups carry the keycloak-admin password, the config client's secret and the
local admin's username, and bin/restore-install.sh writes them back. If neither
the config client nor keycloak-admin can log in, the Job stops and prints what to
do. The commands are in README-BACKUP.md in the distribution, under
Troubleshooting, "keycloak-config Job".
Local admin of the octostar realm
The install makes a local user in the octostar realm, for Octostar access while AD
is down.
- Username:
octostar-admin-and 8 random lowercase letters and digits, unlesskeycloak.localAdmin.usernamesets one. It is generated once. Nobody outside can guess it, so nobody can keep it locked with wrong passwords. - Secret:
keycloak-octostar-adminholds the username and the password. - Email: the made-up address
<username>@<domain>. No mailbox exists for it. The random part makes a match with a real mailbox or AD user unlikely. To use a real address, setkeycloak.localAdmin.email. - Group:
keycloak.localAdmin.group, by defaultoctostar-admins.
For this account to work in Octostar:
- Map its group to an admin role in
oidc.roleMapping, withoidc.allowAdminRoles: true. - Set
oidc.autoRegister: true, or make its Timbr user first. Without one of these, the login is refused.
If you change keycloak.localAdmin.username, the install makes a new user. Delete
the old user in Keycloak. A password policy on the octostar realm must accept a
32-character password of letters and digits, or every sync fails.
Use your own OIDC provider
Do these steps before the install or the update, because the API sends every login
to the provider. Keep keycloak.enabled at false: the install refuses another
issuer while Keycloak is on.
1. Register a client at the provider
Make a confidential web client, with authorization code flow. Register these redirect URIs, both of them:
https://fusion.<domain>/oauth-authorized/oidc
https://home.<domain>/api/auth/oidc/callback
If the provider has an end-session endpoint, also register this post-logout redirect URI. Google has none.
https://home.<domain>/api/auth/oidc/logged-out
A logout from Fusion ends only the Fusion session. It does not end the provider's session.
2. Restrict sign-in to your organization
⚠️ Warning — Octostar accepts every user that the provider signs in for this client. It does not check a domain or a tenant. With
autoRegister: true, each such user gets an Octostar user withdefaultRole. Set the restriction at the provider.
| Provider | What to do |
|---|---|
In the Google Cloud project of your organization, set the OAuth consent screen's audience to Internal. Internal needs a project in a Google Cloud Organization. Without one, you cannot restrict sign-in here, and External lets any Google account sign in. Then use autoRegister: false. | |
| Entra ID | Use the issuer with your tenant ID. To allow only some users, turn on Assignment required on the enterprise application. Assigning a group needs Entra ID P1 or P2. A free tenant can assign only individual users. |
| Okta | Assign only your users or groups to the application. |
With autoRegister: false, only users that already exist in Timbr can log in.
Not verified: we did not test these three rows with a real client. They follow the documentation of each vendor. Test sign-in with an account from outside your organization.
3. Set oidc: in local-env.yaml
oidc:
issuer: "https://login.example.com"
clientId: "<client id>"
clientSecret: "<client secret>"
scopes: ["openid", "email", "profile"]
usernameClaim: "preferred_username"
autoRegister: true
defaultRole: "os_user"
groupsClaim: "groups"
allowAdminRoles: true
roleMapping:
octostar-users: "os_user"
octostar-admins: ["os_user", "os_admin", "admin", "admin_all_workspaces"]
| Key | Default | Meaning |
|---|---|---|
issuer | required | The provider's issuer, from its /.well-known/openid-configuration. https:// only, with no slash at the end. |
clientId, clientSecret | required | The client. The secret goes into the oidc-client-secret Secret. |
scopes | openid, email, profile | Must include openid. Add the provider's groups scope where it has one. |
usernameClaim | required | Names a new user at the first login: preferred_username, email or sub. See the table below. |
emailClaim | none | The claim to use as the email when the token has no verified one |
autoRegister | false | Makes a user at the first login |
defaultRole | required | See Map groups to roles. |
groupsClaim | none | The ID token claim that holds the groups. Omit it for a provider without groups. |
roleMapping | none | Provider group to a Timbr role, or a list of roles |
allowAdminRoles | false | Lets roleMapping grant admin-class roles |
The first login needs an email: a verified email, or the claim that emailClaim
names. usernameClaim: email needs a token with email_verified: true.
usernameClaim per provider:
| Provider | usernameClaim | Why |
|---|---|---|
| Keycloak, with or without AD | preferred_username | The login. A Timbr user with that name keeps the account. |
| Entra ID | preferred_username | The UPN. See Entra ID. |
| Okta | preferred_username | The login |
email | Google has no preferred_username. |
A username must be 1 to 64 characters of a-z 0-9 . _ - + @ after lowercasing. The
API refuses any other name at the first login, because Timbr cannot use it.
The provider's certificate must be publicly trusted. With
octostar.internalDomain, it can also be signed by the install's own CA.
4. Apply and check
- Run
./bin/install.shor./bin/update-existing.sh, with--context <context>. - Log in to
https://home.<domain>as a provider user. - Log in to
https://fusion.<domain>as the same user. - In Fusion's SQL Lab, as
admin, check the user's roles.
Google
Not verified: we rendered this example, but we did not log in with a real Google client in this procedure.
oidc:
issuer: "https://accounts.google.com"
clientId: "<id>.apps.googleusercontent.com"
clientSecret: "<secret>"
scopes: ["openid", "email", "profile"]
usernameClaim: "email"
autoRegister: true
defaultRole: "os_user"
Google tokens carry no groups, so omit groupsClaim and roleMapping. Every user
gets the default role. Grant other roles by hand: see
Users and roles. To turn Google groups into roles, use
Connect Google Workspace.
Entra ID
Not verified: we rendered this example and read the code, but we did not test it with a real Entra client. The Entra settings below follow Microsoft's documentation.
oidc:
issuer: "https://login.microsoftonline.com/<tenant id>/v2.0"
clientId: "<application id>"
clientSecret: "<client secret>"
scopes: ["openid", "email", "profile"]
usernameClaim: "preferred_username"
emailClaim: "preferred_username"
autoRegister: true
defaultRole: "os_user"
groupsClaim: "groups"
allowAdminRoles: true
roleMapping:
octostar-users: "os_user"
- Username and email: set
usernameClaimandemailClaimtopreferred_username, the UPN. Entra never sendsemail_verified, sousernameClaim: emailrefuses every new user. - Identity: the API uses the
oidclaim as the subject, becausesubdiffers per application.oidneeds theprofilescope. - Post-logout redirect: register
https://home.<domain>/api/auth/oidc/logged-outas a redirect URI of the application. Entra needs the post-logout URI to match one of its redirect URIs. - Groups limit: Entra drops the groups claim for a user in more than 200 groups,
nested groups included. So send only Groups assigned to the application, and
assign every group that
roleMappinguses. Entra does not expand nested groups with this setting: the user must be a direct member of an assigned group. - Licence: assigning a group to an application needs Entra ID P1 or P2. On a free tenant, you cannot use Groups assigned to the application. Send Security groups instead. Then the claim holds object IDs and nested groups, and the limit of 200 groups applies.
- Group names: groups arrive as object IDs, unless you set Entra to send names.
Set the claim's source attribute to
sAMAccountNamefor groups synced from AD, or to Cloud-only group display names for cloud groups. Cloud-only names need Groups assigned to the application. Otherwise use the object IDs as theroleMappingkeys. - When the claim is dropped: a returning user's roles do not change, and a first login gets the default role.
- Refused users: a guest whose UPN holds
#EXT#, and anyone whose username claim is over 64 characters, are refused at the first login. Timbr cannot use those names.
Okta
Not verified: we did not test this section with a real Okta client. It follows Okta's documentation.
Use usernameClaim: "preferred_username". The groups claim depends on the authorization
server. Assigning a group to the application does not put the group in the token.
- Org authorization server: on the application's Sign On tab, edit OpenID
Connect ID Token. Set a Groups claim filter with the name
groups. Addgroupstooidc.scopes. - Custom authorization server: on the Claims tab, add a claim named
groups, with the value type Groups and a filter. Include it in the ID token, always.
Move Keycloak or switch provider
A returning user is found by issuer and subject. When either changes, the API does not find the link. The result depends on the username claim at the next login:
| Username claim | autoRegister | Result |
|---|---|---|
| Equals the username of an old link | Any | Refused: "Your Octostar username belongs to another account". |
| A new username | true | The API makes a new, empty Octostar user, with no error. The person does not reach their data. The new link also blocks a later pre-link. |
| A new username | false | Refused: "You have no Octostar account yet". No link stays. |
So move the identity links in the same maintenance window as the change. For any change
of issuer or subject, set oidc.autoRegister: false for the window.
| Change | Issuer | Subject | What to do with the links |
|---|---|---|---|
keycloak.location | Changes | Same | Update the issuer |
A new realm, or a lost keycloak database | Same | Changes | Replace the links |
| Another provider | Changes | Changes | Replace the links |
| First provider of an install with password users | New | New | No links yet. See Move password users to a provider. |
The SQL below is for octostar.metaDb.type: postgresql, the default. With mysql, the
table is in MySQL.
Before each change, take a backup, if the install has S3 backups. See Backup & restore. Rehearse a provider switch on a restored copy of the install.
Move Keycloak to another location
-
For
hostname, add the DNS record first. -
If the realm brokers an identity provider, such as the Google login of a
type: googledirectory, register its new redirect URI at that provider. Otherwise those logins fail after the update. The URI ishttps://auth.<domain>/realms/octostar/broker/<alias>/endpointwithhostname, andhttps://home.<domain>/auth/realms/octostar/broker/<alias>/endpointwithsubpath. Not verified: we read thesubpathredirect URI that Keycloak sends to Google on a test install, but we did not run a location move with a brokered provider. -
Set
keycloak.locationandoidc.autoRegister: falseinlocal-env.yaml. Without the second key, a user renamed at the provider since their first login gets a new, empty user before the links move. -
Run
./bin/update-existing.sh --context <context>. -
Update the links. This example moves from
subpathtohostname. Swap the two URLs to move back.primary=$(kubectl -n octostar-main get pod -l cnpg.io/instanceRole=primary -o name)kubectl -n octostar-main exec -i "$primary" -c postgres -- psql -U postgres -d octostar -c \"UPDATE oidc_identity_links SET issuer = 'https://auth.<domain>/realms/octostar'WHERE issuer = 'https://home.<domain>/auth/realms/octostar';" -
Log in as a returning user, in Octostar and in Fusion.
-
If
oidc.autoRegisterwastrue, set it back, and run the update.
Import the directory users
Do this only when the install has users already, and you must move their links before
anyone logs in. Replace the links and
Move password users to a provider need the
Keycloak id of each directory user. Keycloak imports a directory user at the first
login or at a sync, so run a full sync now.
With kc from Add users without a directory:
id=$(kc get components -r octostar -q type=org.keycloak.storage.UserStorageProvider -q name=<directory name> --fields id --format csv --noquotes)
kc create "user-storage/$id/sync?action=triggerFullSync" -r octostar -o
<directory name> is the entry's name in keycloak.ldap: by default
active-directory or google-workspace. With several directories, run the sync for
each of them. The answer shows the number of users that Keycloak imported.
Replace the links
⚠️ Not rehearsed — Nobody ran this procedure end to end yet. We ran only its SQL statements. Run it first on a restored copy of the install.
-
Compare the Timbr users with the accounts at the new provider, and fix these cases before the switch. Leave out
admin,platform,os_systemand octostar-api's service account (TIMBR_WRITE_USERNAME). The API reserves these names, so nobody can adopt them, and the platform needs them active.This paragraph is only for a switch from another provider to the bundled Keycloak. The install refuses Keycloak while
oidc:names the other provider, so the Keycloak accounts do not exist before the update in step 2. That update also sends logins to Keycloak. So compare against AD, or against the list of users that you will make, before that update. After it, the first account with the name of an unlinked Timbr user adopts that user, also withautoRegister: false. Move password users to a provider has another order, because there Keycloak runs before logins go to it.Case Risk Action A Timbr user has no account of the same username A recycled name can adopt the user. Deactivate the Timbr user, or pre-link it. A provider account has the username of another person's Timbr user That person's account is adopted. Rename the account, or pre-link the Timbr user. A Timbr username holds characters other than a-z 0-9 . _ - + @The user cannot log in through the provider. Decide before the switch what this person uses. -
Set
oidc.autoRegister: false. Change the provider, then run./bin/update-existing.sh --context <context>.- To your own provider: set
keycloak.enabled: false, and addoidc.issuer,oidc.clientIdandoidc.clientSecret. - To the bundled Keycloak: remove those three keys, and set
keycloak.enabled: true. The install refuses Keycloak while another issuer is set, so the Keycloak users do not exist before this update. After the update, make the users or connect AD. Their Keycloakidvalues for a pre-link exist only from then on.
- To your own provider: set
-
List the links:
primary=$(kubectl -n octostar-main get pod -l cnpg.io/instanceRole=primary -o name)kubectl -n octostar-main exec -i "$primary" -c postgres -- psql -U postgres -d octostar -c \"SELECT issuer, subject, username, last_login_at FROM oidc_identity_links ORDER BY username;" -
Copy the table and count the rows, in the same
psqlway. Make sure that the count equals the number of rows with this issuer in step 3.CREATE TABLE oidc_identity_links_backup AS TABLE oidc_identity_links;SELECT count(*) FROM oidc_identity_links WHERE issuer = '<old issuer>'; -
Do one of these, in the same
psqlway:-
The username stays the same at the new provider. Delete the old links. The next login adopts the Timbr user by name.
⚠️ Warning — Without the copy from step 4, you cannot undo this
DELETE. Each link also holds the roles that the mapping granted, and theDELETEloses them.DELETE FROM oidc_identity_links WHERE issuer = '<old issuer>';After a lost
keycloakdatabase, the old issuer is the current issuer.The API then does not know which roles the old mapping granted. It treats them as granted by hand and does not revoke them. Check the roles of users who changed group.
-
The username differs, or you want to keep the mapped roles. Pre-link each user. The subject is the user's
subat the new provider, oroidon Entra ID. In Keycloak, it is the user'sid.A directory user has no Keycloak
iduntil Keycloak imports the user, at the first login or at a sync. To import the users now, see Import the directory users. Not verified with AD: the sync ran only on a test OpenLDAP server. Then read theid:kc get users -r octostar -q exact=true -q username=<ad login in lowercase> --fields id --format csv --noquotesUse these values in the SQL.
<new issuer>isoidc.issuerwith no slash at the end, because the API looks up the link by the issuer without that slash.<timbr username>is in lowercase, because a link stores it so. Make sure that the result isUPDATE 1.UPDATE oidc_identity_links SET issuer = '<new issuer>', subject = '<new subject>'WHERE username = '<timbr username>';For a user who has no link yet, add one, with the same values:
INSERT INTO oidc_identity_links (issuer, subject, username, managed_roles)VALUES ('<new issuer>', '<new subject>', '<timbr username>', '[]');For many users, make the statements from a file. With a Google directory, for example, each person's Keycloak user has their Google email, and their Timbr user has another name. Write
mapping.csvwith one<timbr username>,<email>line for each person, and end the file with a line break. Then read the Keycloakidof each email, and write one statement for each line. A line for a person who has a link updates it and keeps its mapped roles. A line for a person with no link adds one:kubectl -n octostar-main exec keycloak-0 -- bash -c 'k=/opt/keycloak/bin/kcadm.sh; f=$(mktemp)KC_CLI_PASSWORD=$KC_BOOTSTRAP_ADMIN_PASSWORD $k config credentials --config "$f" \--server "http://localhost:8080${KC_HTTP_RELATIVE_PATH:-}" --realm master --user "$KC_BOOTSTRAP_ADMIN_USERNAME" >/dev/null 2>&1$k get users --config "$f" -r octostar -l 1000 --fields id,email --format csv --noquotesrm -f "$f"' > keycloak-users.csv{ echo "BEGIN;"while IFS=, read -r user email; doid=$(awk -F, -v e="$email" 'e != "" && tolower($2) == tolower(e) {print $1}' keycloak-users.csv)if [ -n "$id" ]; thenecho "INSERT INTO oidc_identity_links (issuer, subject, username, managed_roles) VALUES ('<new issuer>', '$id', lower('$user'), '[]') ON CONFLICT (username) DO UPDATE SET issuer = EXCLUDED.issuer, subject = EXCLUDED.subject;"elseecho "-- NO KEYCLOAK USER FOR $user ($email)"fidone < mapping.csvecho "COMMIT;"; } > links.sqlRead
links.sql. Fix everyNO KEYCLOAK USERline, then apply the file. It is one transaction, so if one row fails, no row stays.primary=$(kubectl -n octostar-main get pod -l cnpg.io/instanceRole=primary -o name)kubectl -n octostar-main exec -i "$primary" -c postgres -- psql -U postgres -d octostar -v ON_ERROR_STOP=1 < links.sqlIf a new, empty user appeared because
autoRegisterwastrue, first delete its link. Otherwise the pre-link fails on the unique constraint. Then deactivate that Timbr user.DELETE FROM oidc_identity_links WHERE issuer = '<new issuer>' AND subject = '<new subject>';
-
-
Log in as a returning user. Check their ownership, workspace permissions and roles. To go back, change
oidc:andkeycloak.enabledback, run the update, and then restore the copy. If you restore the copy and do not change them back, the API refuses every returning user.DELETE FROM oidc_identity_links;INSERT INTO oidc_identity_links SELECT * FROM oidc_identity_links_backup; -
When the check passes, drop the copy. If new users must get an account at their first login, set
oidc.autoRegister: trueagain, and run the update.DROP TABLE oidc_identity_links_backup;
Deactivate a Timbr user
https://fusion.<domain>/users/list/ has an Is Active? field on the edit page of each
user. Not verified: we saw this field on a test install, but we did not run a
deactivation. After the change, run SHOW USERS in Fusion's SQL Lab as admin, and make
sure that its active column shows the user as inactive. The API refuses a login
according to this column.
Move password users to a provider
⚠️ Not rehearsed — Nobody ran this procedure end to end yet. Run it first on a restored copy of the install.
An existing install has its users in Timbr, and it logs in through Fusion until you add
oidc:.
-
With the bundled Keycloak, set only
keycloak:, with nooidc:section, and run./bin/update-existing.sh --context <context>. Keycloak starts, and users still log in through Fusion. -
With the bundled Keycloak, make the Keycloak users with their Timbr usernames and emails, so that each login adopts its Timbr user:
- Skip users who come from a directory. Import the directory users, so that those users exist in Keycloak.
- Leave out
admin,os_system,platformand octostar-api's service account (TIMBR_WRITE_USERNAME). - Set temporary passwords, and give them out of band. Or, for test accounts, keep their Timbr passwords.
With your own provider, only make sure that its usernames match the Timbr usernames.
-
Do step 1 of Replace the links. Do it now, not earlier: before step 2, no Keycloak account exists, and every Timbr user would fall into the first row of that table. A pre-link here is an
INSERT, because this install has no links yet. -
Add
oidc:withautoRegister: false, and run the update again. Now every login goes to the provider. The local admin needs a Timbr user, orautoRegister: true. -
Log in as a migrated user. Check their ownership, workspace permissions, mounts and roles.
-
If new users must get an account at their first login, set
oidc.autoRegister: true, and run the update.
Keep the Timbr passwords
Keycloak can take each user's Timbr password hash as it is. The user then logs in to
Keycloak with the same password as before, and nobody needs to know it. Timbr stores
pbkdf2:sha256 hashes, and Keycloak checks them as pbkdf2-sha256.
Use this only for test accounts. Step 2 gives a user with no email a made-up address, and gives every user the username as first and last name. For real people, make the users as in Add users without a directory, with their real names and emails.
-
List the users, with their hashes. Only you can read the file, and you delete it at the end.
NOT INleaves out the reserved names and anyone who gets another login, such as the people of a directory.umask 077primary=$(kubectl -n octostar-main get pod -l cnpg.io/instanceRole=primary -o name)kubectl -n octostar-main exec "$primary" -c postgres -- psql -U postgres -At -F'|' -d timbr -c \"SELECT username, email, password FROM timbr_server.ab_userWHERE username NOT IN ('admin', 'platform', 'os_system', '<service account>', '<other names>')AND password LIKE 'pbkdf2:sha256:%' ORDER BY username" > timbr-users.psvcut -d'|' -f1 timbr-users.psv -
Turn each line into a Keycloak user. A user with no email gets
<username>@example.test, because the API needs an email.cat > timbr-to-keycloak.py <<'PY'import base64, json, sysfor line in sys.stdin:user, email, pw = line.rstrip("\n").split("|")method, salt, digest = pw.split("$")iterations = int(method.split(":")[2]) # pbkdf2:sha256:<iterations>secret = {"value": base64.b64encode(bytes.fromhex(digest)).decode(),"salt": base64.b64encode(salt.encode()).decode(), "additionalParameters": {}}data = {"hashIterations": iterations, "algorithm": "pbkdf2-sha256", "additionalParameters": {}}print(json.dumps({"username": user, "email": email if "@" in email else f"{user}@example.test","emailVerified": True, "enabled": True, "firstName": user, "lastName": user,"credentials": [{"type": "password", "secretData": json.dumps(secret),"credentialData": json.dumps(data)}]}))PYpython3 timbr-to-keycloak.py < timbr-users.psv > keycloak-users.jsonl -
Make the users, then delete the files. Each user prints
Created new user.Conflictmeans that the user exists already.kubectl -n octostar-main exec -i keycloak-0 -- bash -c 'k=/opt/keycloak/bin/kcadm.sh; f=$(mktemp)KC_CLI_PASSWORD=$KC_BOOTSTRAP_ADMIN_PASSWORD $k config credentials --config "$f" \--server "http://localhost:8080${KC_HTTP_RELATIVE_PATH:-}" --realm master --user "$KC_BOOTSTRAP_ADMIN_USERNAME" >/dev/null 2>&1while IFS= read -r user; do printf "%s\n" "$user" | $k create users --config "$f" -r octostar -f -; donerm -f "$f"' < keycloak-users.jsonlrm -f timbr-users.psv keycloak-users.jsonl
At the first login, each user adopts the Timbr user of the same name, with its roles.
A user in no mapped group also gets defaultRole at each login.
The copied hash does not follow later changes. A Timbr password set after the move, such as the random password below, does not change the Keycloak password.
⚠️ Warning — The
oidc:section only hides Fusion's password form. The form at/login/?local=1, Fusion's login API and/timbr/api/get_tokenkeep accepting any password that works.
To close this, give each migrated user a random Timbr password. Use letters and digits
only: Timbr cannot use ", and the token exchange fails on \. In Fusion's SQL Lab, as
admin:
ALTER USER `alice` SET PASSWORD = '<random letters and digits>';
First make sure that the user does not log in to Timbr directly with a password, for
example from JDBC or a BI tool. Change only the passwords of migrated people. The
service accounts keep their passwords, because they get their Timbr tokens with them:
octostar-api's service account, the scheduler, the apps-upgrade job, init-datasources,
the ontology-creation helper, and admin, which platform key rotation uses.
Recover when the keycloak database is gone
keycloak-0 crash-loops with database "keycloak" does not exist, and the
keycloak-config Job fails. CNPG does not create the database again: its Database
resource keeps applied: true, also through an update.
First choose how to recover. To get the old users and subjects back, restore the install from a backup: see Backup & restore. The steps below make an empty database.
-
Create the database on the CNPG primary:
primary=$(kubectl -n octostar-main get pod -l cnpg.io/instanceRole=primary -o name)kubectl -n octostar-main exec -i "$primary" -c postgres -- psql -U postgres -c \"CREATE DATABASE keycloak OWNER keycloak ENCODING 'UTF8' TEMPLATE template1;" -
Delete the pod. Keycloak starts on the empty database, and makes the
masterrealm'sadminfrom thekeycloak-adminSecret.kubectl -n octostar-main delete pod keycloak-0 -
Set
oidc.autoRegister: false, and run the update. Thekeycloak-configJob applies theoctostarrealm and the local admin again../bin/update-existing.sh --context <context> -
Make again what is not in the configuration: local Keycloak users, group memberships of local users, and objects made in the console. AD users come back from AD at their next login or sync.
-
Replace the links, from step 3. The issuer is the same, but every user has a new subject, so the API refuses returning users until then. A user renamed in AD since their first login has a new username, so pre-link that user.
Troubleshooting
| What you see | Cause | Action |
|---|---|---|
| "Your Octostar username belongs to another account" | Another identity holds this username: the issuer or the subject changed. | Move the links. |
| "You have no Octostar account yet" | autoRegister is false, and the Timbr user does not exist. | Make the user, or set autoRegister: true. |
| "Your Octostar account is deactivated" | The Timbr user is not active. | Activate the user. See Deactivate a Timbr user, which is not verified. |
| "Your first login is already in progress. Try again." | Two logins at the same time, or a link deleted during the login. | Log in again. |
| "Your account at the identity provider has no email address" | A first login needs an email. | Set the email at the provider, or set emailClaim. |
| "Your account cannot log in to Octostar" | For example: a name with #EXT#, a name over 64 characters, a reserved name, no email_verified with usernameClaim: email, or no oid on Entra ID. | Read the octostar-api log. See the username rules. |
| The provider says that the redirect URI is wrong | The URI is not registered. | Register both redirect URIs. |
| A user can log in to Octostar but not to Fusion | Fusion needs the link that the first Octostar login makes. | Log in to https://home.<domain> first. |
| A user has the wrong roles | The group name does not equal a roleMapping key, or the user did not log in to Octostar after the change. | Read the token's groups claim. Log in to Octostar again. |
| An AD user cannot log in, and AD is fine | Keycloak locked the account. | Wait, or clear the lock. |
| nginx answers 503 on the login form | Above keycloak.loginConnectionsPerIp. | Raise the limit, or set 0. |
keycloak-0 logs database "keycloak" does not exist | The database is gone. | Recover it. |
| The Job prints "The realm configuration cannot be applied" | Neither keycloak-config-client nor keycloak-admin can log in. | See Recovery access. |
The install stops with oidc.issuer is ..., but keycloak.enabled is true | keycloak.enabled is true, and the issuer names another provider. | Remove the issuer, or set keycloak.enabled: false. |
The install stops with oidc.issuer is required, as a string, ... | Keycloak is off, and oidc: names no issuer. | See Turn Keycloak off. |
The install stops with oidc.roleMapping.<group> grants ... | The mapping grants an admin-class role. | Set oidc.allowAdminRoles: true, or remove the role. |