Skip to content

Auth0, Keycloak and generic OIDC

Auth0 has no roles claim. You add one with a Login Action:

exports.onExecutePostLogin = async (event, api) => {
const ns = "https://seylo/roles";
api.idToken.setCustomClaim(ns, event.authorization?.roles ?? []);
};

Then in Seylo:

  • Application type: Single Page Application.
  • Identifier: your Auth0 domain.
  • Role claim: the exact namespaced stringhttps://seylo/roles in the example above.
  • Discriminator: none, unless you use Auth0 Organizations, in which case set org_id. One Auth0 tenant can serve many customers, and without it they share an issuer.
  • Client: public, standard flow, redirect URI = the portal’s origin.
  • Preset Keycloak, identifier = the realm URL, e.g. https://sso.acme.example/realms/acme. Discovery is appended to that path, not substituted, so the realm path is preserved.
  • The role claim is nested. Realm roles live at realm_access.roles; client roles at resource_access.<your-client-id>.roles. Both work as dotted paths, and the preset defaults to the realm path.
  • Include the roles scope, or add a dedicated role mapper — otherwise neither claim appears in the token at all.

Supply the discovery URL — or the issuer — and the name of the claim your roles arrive in. Requirements:

  • Discovery must be served over https, and the document must declare the issuer it was fetched from. A document served from one host claiming to speak for another is refused deliberately: its keys could otherwise mint tokens for that issuer.
  • jwks_uri is required and must be https.
  • Only asymmetric signing algorithms are accepted.

If several of your customer organisations share one issuer, set a discriminator claim and value. Otherwise the second organisation registered cannot be told apart from the first, and Seylo will refuse the registration rather than guess.

For a role claim, all of these are understood:

  • an array — ["releaser", "approver"]
  • a space- or comma-delimited string — "releaser approver"
  • a single scalar — "admin"

The discriminator is deliberately not split. A value of "evil.example acme.example" must not match acme.example.