Skip to main content

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.

OptionUse it whenSettings in local-env.yaml
Password login (default)You do not need single sign-on.None
Bundled KeycloakYou 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 providerYou 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: true with an oidc: 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_links table. 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.locationURLIssuerWhat you need
hostname (recommended)https://auth.<domain>https://auth.<domain>/realms/octostarA DNS record or a wildcard for auth.<domain>, to where home.<domain> points
subpath (default)https://home.<domain>/authhttps://home.<domain>/auth/realms/octostarNothing 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:

KeyDefaultMeaning
replicas1Keycloak pods
db.poolSize20PostgreSQL connections per pod
clientIdoctostarThe client that Fusion and the API use
groupsoctostar-users, octostar-adminsGroups that the install creates. Removing a name does not delete the group.
adminConsole.enabledfalseRoutes /admin/ and /realms/master/
adminPasswordempty (generated)Password of the master realm's admin: 16 or more of A-Z a-z 0-9 . _ ~ -
localAdmin.username, localAdmin.group, localAdmin.emailgenerated, octostar-admins, generatedSee Local admin
loginConnectionsPerIp0 (no limit)Login posts in progress per client address
theme.enabledtrueThe Octostar look on the login pages
config.enabledtruefalse 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​

  1. Read the issuer. Use home.<domain>/auth in place of auth.<domain> with subpath:

    curl -sk https://auth.<domain>/realms/octostar/.well-known/openid-configuration | jq .issuer
  2. Read the local admin's username and password:

    kubectl -n octostar-main get secret keycloak-octostar-admin -o jsonpath='{.data.username}' | base64 -d
    kubectl -n octostar-main get secret keycloak-octostar-admin -o jsonpath='{.data.password}' | base64 -d
  3. Open https://home.<domain> and log in as the local admin.

  4. 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/oidc and https://home.<domain>/api/auth/oidc/callback.
  • The post-logout redirect URIs: https://home.<domain>/api/auth/oidc/logged-out, https://home.<domain>/ and https://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 octostar and master realms.

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.clientId and oidc.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, as admin. 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​

  1. Make a read-only service account for the bind. Any domain user can read users and groups.
  2. 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.
  3. Get the CA certificate, in PEM format, when Keycloak's JVM does not trust it.
  4. Make sure that the host name in the connection URL is the name in the certificate.
  5. Name the AD groups as the oidc.roleMapping keys, and put them under one groups DN.
  6. 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-----
KeyDefaultMeaning
typerequiredad. For google, see Connect Google Workspace.
nameactive-directoryKeycloak's provider name: 1 to 32 of a-z 0-9 -. See the warning below.
enabledtruefalse keeps the provider and its users, and blocks their logins.
connectionUrlrequiredldaps://<host>[:port], or ldap://<host>[:port] with startTls: true
startTlsfalseUse StartTLS on an ldap:// URL.
bindDn, bindPasswordrequiredThe service account. The password goes into the keycloak-ldap Secret, and a sync applies a change with no Keycloak restart.
usersDnrequiredWhere the users are. The search covers the whole subtree.
userFilter(objectCategory=person)Which users to import
groupsDnrequiredOnly groups under this DN reach the token.
groupFilteremptyWhich groups to import
caCertemptyThe CA of the AD certificate, in PEM format. Restart Keycloak after a change.
hostAliasesemptyA list of {ip, hostnames}, when the cluster DNS does not resolve the AD host
masterRealmfalseAdds 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.
fullSyncPeriodSeconds86400Full sync period. -1: no periodic sync.
changedSyncPeriodSeconds3600Changed-users sync period. -1: no periodic sync.

⚠️ Warning — Never change name after 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, use enabled: 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 keyDefaultRule with AD
failureFactor3Below AD's lockout threshold
waitIncrementSeconds1200Above AD's "reset account lockout counter after" time
maxFailureWaitSeconds1200waitIncrementSeconds or more
failureResetSeconds43200The 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​

  1. Run ./bin/update-existing.sh --context <context>.

  2. Log in to https://home.<domain> as an AD user who is in a mapped group.

  3. 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:

ClaimValue
preferred_usernameThe AD login, in lowercase. It becomes the Timbr username, so a Timbr user with that name keeps the account.
upnThe userPrincipalName
groupsThe user's AD groups under groupsDn, by name. Nested groups count.
emailThe 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​

  1. 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.
  2. In Google Admin, Apps > Google Workspace > Groups for Business > Sharing settings, let only admins make groups. A group named as a roleMapping key grants its roles.
  3. 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/endpoint
    • hostname: 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-----
...
KeyDefaultMeaning
baseDnrequiredThe Workspace domain, such as dc=example,dc=com
groupFilterrequiredThe groups that reach the token. List only groups that admins own.
clientCert, clientKeyrequiredThe LDAP client's certificate and key, from step 1
namegoogle-workspaceKeycloak'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
SettingWhy
hostedDomainGoogle offers only accounts of your Workspace domain.
firstBrokerLoginFlowAlias: link-existing-userA 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: trueThe API needs a verified email at a first login.
syncMode: IMPORTThe 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​

  1. Add the oidc: section, with groupsClaim and a roleMapping whose keys are the Google group names, and run the update.
  2. Open https://home.<domain>. The Keycloak page shows a Google button.
  3. 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:

ClaimValue
preferred_usernameThe full email. It becomes the Timbr username at the first login. A Timbr user with another name needs a pre-link.
groupsThe user's Google groups that groupFilter matches, by name. Only direct members.
emailThe 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_user to every user. It gives ontology access. Fusion's editor, analyst and viewer give no ontology access.
  • defaultRole applies when no group maps. It is required, and it cannot be an admin-class role.
  • Admin-class roles are admin, os_admin and every *_all_workspaces role. roleMapping can grant them only with allowAdminRoles: true.
  • roleMapping needs groupsClaim. The install refuses roleMapping without 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​

ProblemAccountWhere the credentials areHow to log in
AD or Google is down, Keycloak is upLocal admin of the octostar realmkeycloak-octostar-admin Secrethttps://home.<domain>
Keycloak or your own provider is downFusion's admintimbrAdminPasswordhttps://fusion.<domain>/login/?local=1
You must administer Keycloakadmin of the master realmkeycloak-admin Secretkcadm.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, unless keycloak.localAdmin.username sets one. It is generated once. Nobody outside can guess it, so nobody can keep it locked with wrong passwords.
  • Secret: keycloak-octostar-admin holds 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, set keycloak.localAdmin.email.
  • Group: keycloak.localAdmin.group, by default octostar-admins.

For this account to work in Octostar:

  1. Map its group to an admin role in oidc.roleMapping, with oidc.allowAdminRoles: true.
  2. 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 with defaultRole. Set the restriction at the provider.

ProviderWhat to do
GoogleIn 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 IDUse 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.
OktaAssign 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"]
KeyDefaultMeaning
issuerrequiredThe provider's issuer, from its /.well-known/openid-configuration. https:// only, with no slash at the end.
clientId, clientSecretrequiredThe client. The secret goes into the oidc-client-secret Secret.
scopesopenid, email, profileMust include openid. Add the provider's groups scope where it has one.
usernameClaimrequiredNames a new user at the first login: preferred_username, email or sub. See the table below.
emailClaimnoneThe claim to use as the email when the token has no verified one
autoRegisterfalseMakes a user at the first login
defaultRolerequiredSee Map groups to roles.
groupsClaimnoneThe ID token claim that holds the groups. Omit it for a provider without groups.
roleMappingnoneProvider group to a Timbr role, or a list of roles
allowAdminRolesfalseLets 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:

ProviderusernameClaimWhy
Keycloak, with or without ADpreferred_usernameThe login. A Timbr user with that name keeps the account.
Entra IDpreferred_usernameThe UPN. See Entra ID.
Oktapreferred_usernameThe login
GoogleemailGoogle 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​

  1. Run ./bin/install.sh or ./bin/update-existing.sh, with --context <context>.
  2. Log in to https://home.<domain> as a provider user.
  3. Log in to https://fusion.<domain> as the same user.
  4. 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 usernameClaim and emailClaim to preferred_username, the UPN. Entra never sends email_verified, so usernameClaim: email refuses every new user.
  • Identity: the API uses the oid claim as the subject, because sub differs per application. oid needs the profile scope.
  • Post-logout redirect: register https://home.<domain>/api/auth/oidc/logged-out as 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 roleMapping uses. 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 sAMAccountName for 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 the roleMapping keys.
  • 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. Add groups to oidc.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 claimautoRegisterResult
Equals the username of an old linkAnyRefused: "Your Octostar username belongs to another account".
A new usernametrueThe 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 usernamefalseRefused: "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.

ChangeIssuerSubjectWhat to do with the links
keycloak.locationChangesSameUpdate the issuer
A new realm, or a lost keycloak databaseSameChangesReplace the links
Another providerChangesChangesReplace the links
First provider of an install with password usersNewNewNo 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​

  1. For hostname, add the DNS record first.

  2. If the realm brokers an identity provider, such as the Google login of a type: google directory, register its new redirect URI at that provider. Otherwise those logins fail after the update. The URI is https://auth.<domain>/realms/octostar/broker/<alias>/endpoint with hostname, and https://home.<domain>/auth/realms/octostar/broker/<alias>/endpoint with subpath. Not verified: we read the subpath redirect URI that Keycloak sends to Google on a test install, but we did not run a location move with a brokered provider.

  3. Set keycloak.location and oidc.autoRegister: false in local-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.

  4. Run ./bin/update-existing.sh --context <context>.

  5. Update the links. This example moves from subpath to hostname. 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';"
  6. Log in as a returning user, in Octostar and in Fusion.

  7. If oidc.autoRegister was true, 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.

⚠️ 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.

  1. Compare the Timbr users with the accounts at the new provider, and fix these cases before the switch. Leave out admin, platform, os_system and 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 with autoRegister: false. Move password users to a provider has another order, because there Keycloak runs before logins go to it.

    CaseRiskAction
    A Timbr user has no account of the same usernameA 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 userThat 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.
  2. Set oidc.autoRegister: false. Change the provider, then run ./bin/update-existing.sh --context <context>.

    • To your own provider: set keycloak.enabled: false, and add oidc.issuer, oidc.clientId and oidc.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 Keycloak id values for a pre-link exist only from then on.
  3. 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;"
  4. Copy the table and count the rows, in the same psql way. 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>';
  5. Do one of these, in the same psql way:

    • 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 the DELETE loses them.

      DELETE FROM oidc_identity_links WHERE issuer = '<old issuer>';

      After a lost keycloak database, 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 sub at the new provider, or oid on Entra ID. In Keycloak, it is the user's id.

      A directory user has no Keycloak id until 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 the id:

      kc get users -r octostar -q exact=true -q username=<ad login in lowercase> --fields id --format csv --noquotes

      Use these values in the SQL. <new issuer> is oidc.issuer with 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 is UPDATE 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.csv with one <timbr username>,<email> line for each person, and end the file with a line break. Then read the Keycloak id of 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 --noquotes
      rm -f "$f"' > keycloak-users.csv

      { echo "BEGIN;"
      while IFS=, read -r user email; do
      id=$(awk -F, -v e="$email" 'e != "" && tolower($2) == tolower(e) {print $1}' keycloak-users.csv)
      if [ -n "$id" ]; then
      echo "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;"
      else
      echo "-- NO KEYCLOAK USER FOR $user ($email)"
      fi
      done < mapping.csv
      echo "COMMIT;"; } > links.sql

      Read links.sql. Fix every NO KEYCLOAK USER line, 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.sql

      If a new, empty user appeared because autoRegister was true, 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>';
  6. Log in as a returning user. Check their ownership, workspace permissions and roles. To go back, change oidc: and keycloak.enabled back, 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;
  7. When the check passes, drop the copy. If new users must get an account at their first login, set oidc.autoRegister: true again, 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:.

  1. With the bundled Keycloak, set only keycloak:, with no oidc: section, and run ./bin/update-existing.sh --context <context>. Keycloak starts, and users still log in through Fusion.

  2. 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, platform and 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.

  3. 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.

  4. Add oidc: with autoRegister: false, and run the update again. Now every login goes to the provider. The local admin needs a Timbr user, or autoRegister: true.

  5. Log in as a migrated user. Check their ownership, workspace permissions, mounts and roles.

  6. 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.

  1. List the users, with their hashes. Only you can read the file, and you delete it at the end. NOT IN leaves out the reserved names and anyone who gets another login, such as the people of a directory.

    umask 077
    primary=$(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_user
    WHERE username NOT IN ('admin', 'platform', 'os_system', '<service account>', '<other names>')
    AND password LIKE 'pbkdf2:sha256:%' ORDER BY username" > timbr-users.psv
    cut -d'|' -f1 timbr-users.psv
  2. 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, sys
    for 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)}]}))
    PY
    python3 timbr-to-keycloak.py < timbr-users.psv > keycloak-users.jsonl
  3. Make the users, then delete the files. Each user prints Created new user. Conflict means 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>&1
    while IFS= read -r user; do printf "%s\n" "$user" | $k create users --config "$f" -r octostar -f -; done
    rm -f "$f"' < keycloak-users.jsonl
    rm -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_token keep 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.

  1. 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;"
  2. Delete the pod. Keycloak starts on the empty database, and makes the master realm's admin from the keycloak-admin Secret.

    kubectl -n octostar-main delete pod keycloak-0
  3. Set oidc.autoRegister: false, and run the update. The keycloak-config Job applies the octostar realm and the local admin again.

    ./bin/update-existing.sh --context <context>
  4. 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.

  5. 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 seeCauseAction
"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 wrongThe URI is not registered.Register both redirect URIs.
A user can log in to Octostar but not to FusionFusion needs the link that the first Octostar login makes.Log in to https://home.<domain> first.
A user has the wrong rolesThe 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 fineKeycloak locked the account.Wait, or clear the lock.
nginx answers 503 on the login formAbove keycloak.loginConnectionsPerIp.Raise the limit, or set 0.
keycloak-0 logs database "keycloak" does not existThe 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 truekeycloak.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.