Identity Federation for Citizen Developers, Keycloak and Auth0
TL;DR — Citizen developers should sign into your low-code platforms with corporate SSO, not local accounts. Keycloak 25 and Auth0 both do this cleanly via SAML or OIDC. Wire group claims into role mapping, treat workflow ownership as identity-bound, and rotate offboarding through the IdP, not the platform admin UI.
The fastest way to discover that you have no real identity story for citizen developers is to onboard your first ten of them, hand out individual platform passwords, and then watch what happens at month six when two of them have left the company and nobody can remember which workflows they owned.
That’s the failure mode. The fix is identity federation. The IdP becomes the single source of truth — who exists, what groups they’re in, what they can touch. The platforms (n8n, Make, the gateway, the connector library) ask the IdP at every interaction.
This post walks through the two stacks I’ve shipped most often in 2024 — Keycloak 25 for organisations that want self-hosted, and Auth0 for organisations that don’t. The shape is the same. The integration details differ.
If you’ve followed the post on self-hosting n8n, you’ll remember I said “use SSO, not local accounts.” This is where I actually show how.
What identity federation buys you
Three concrete wins, all of which I’ve watched companies discover the expensive way.
One offboarding lever. When someone leaves, you disable their IdP account. Their access to n8n, Make, the gateway, everything, goes dark in seconds. No “which dashboards do we need to remember to lock them out of.”
Group-based access without per-platform config. Your IdP already knows that Alice is in finance-ops. That group claim flows into n8n as a role, into Make as a team, into Kong as an ACL group. You provision groups once.
Audit trails that join up. Every action across every low-code surface is tagged with the same subject identifier. When the auditor asks “what did Alice do last quarter,” you have one query, not five.
Without federation, each of those is bespoke per platform. With federation, the IdP does the heavy lifting.
Keycloak 25, the self-hosted path
Keycloak 25 (released April 2024, with maintenance updates through September) is the open-source IdP I default to for self-hosted setups. It speaks OIDC and SAML, has a sensible admin UI, and the Keycloak documentation on OIDC client configuration is decent.
The setup against n8n looks like this. n8n 1.58 supports SAML SSO out of the box on the paid plan. For OIDC, you can do it on the community edition via the OIDC plugin, or front the whole thing with oauth2-proxy and have Keycloak handle it that way. I prefer the proxy path for the community edition because it’s also how I gate everything else (the gateway admin UI, the metrics dashboard, the secret manager).
Keycloak realm setup
Create a realm for citizen developers. Keep it separate from your engineering realm if you have one — different password policies, different MFA requirements, different lifecycle.
A realm export, trimmed, showing the relevant pieces for n8n:
{
"realm": "citizen-devs",
"enabled": true,
"loginTheme": "keycloak",
"passwordPolicy": "length(12) and notUsername and forceExpiredPasswordChange(90)",
"otpPolicyType": "totp",
"browserFlow": "browser",
"clients": [
{
"clientId": "n8n",
"protocol": "openid-connect",
"publicClient": false,
"secret": "<from-vault>",
"redirectUris": [
"https://n8n.internal.example.com/oauth2/callback"
],
"webOrigins": ["https://n8n.internal.example.com"],
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"attributes": {
"access.token.lifespan": "900",
"client.session.idle.timeout": "1800"
},
"protocolMappers": [
{
"name": "groups",
"protocol": "openid-connect",
"protocolMapper": "oidc-group-membership-mapper",
"config": {
"claim.name": "groups",
"full.path": "false",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}
]
}
],
"groups": [
{ "name": "citizen-devs-marketing" },
{ "name": "citizen-devs-finance" },
{ "name": "citizen-devs-sales" }
]
}
The groups claim is the load-bearing piece. When a user authenticates, their JWT carries an array like ["citizen-devs-marketing"]. Downstream services map that claim to roles.
Wiring oauth2-proxy in front of n8n
For the community edition path, oauth2-proxy sits between the user and n8n. It enforces login and injects identity headers.
A minimal oauth2-proxy.cfg:
provider = "keycloak-oidc"
client_id = "n8n"
client_secret = "${OAUTH2_PROXY_CLIENT_SECRET}"
oidc_issuer_url = "https://kc.internal.example.com/realms/citizen-devs"
redirect_url = "https://n8n.internal.example.com/oauth2/callback"
email_domains = ["example.com"]
upstreams = ["http://n8n-main:5678/"]
cookie_secret = "${OAUTH2_PROXY_COOKIE_SECRET}"
cookie_secure = true
cookie_samesite = "lax"
pass_access_token = true
pass_authorization_header = true
set_xauthrequest = true
allowed_groups = ["citizen-devs-marketing", "citizen-devs-finance", "citizen-devs-sales"]
What this does: only users in one of the allowed Keycloak groups can reach n8n at all. Their identity is passed downstream via X-Forwarded-Email and the access token. n8n sees an authenticated request via its setup with N8N_USER_MANAGEMENT_DISABLED=true and trusts the proxy. Make sure the proxy is on a network path the user can’t bypass — same VPC, no direct routing to n8n’s pod.
For the paid n8n plan with native SAML, the integration is in the Keycloak admin UI as a SAML client, with n8n’s SP metadata imported. The shape is the same, just SAML instead of OIDC.
Auth0, the SaaS path
Auth0 is the same shape with fewer moving parts. You create an application, set callback URLs, configure connections (database, social, enterprise), and define rules or actions for claim shaping.
The Auth0 piece I keep emphasising to teams is the Action that shapes the access token. By default, group claims live in the user’s app_metadata, not in the token. You write a small Action to push them into the token at issue time.
// Auth0 Post-Login Action
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'https://acme.example.com';
const groups = event.user.app_metadata?.groups || [];
if (groups.length > 0) {
api.idToken.setCustomClaim(`${namespace}/groups`, groups);
api.accessToken.setCustomClaim(`${namespace}/groups`, groups);
}
if (event.user.email_verified !== true) {
api.access.deny('Email not verified');
}
};
The namespace prefix is required for custom claims that flow into access tokens. Without it Auth0 strips them. Painful first time, fine once you know.
The same oauth2-proxy deployment from above changes only its provider config:
provider = "oidc"
oidc_issuer_url = "https://acme.us.auth0.com/"
scope = "openid email profile"
allowed_groups = ["citizen-devs-marketing", "citizen-devs-finance", "citizen-devs-sales"]
oidc_groups_claim = "https://acme.example.com/groups"
Auth0 vs Keycloak is mostly a build-vs-buy question. Keycloak is more flexible, and I’ve never been blocked by anything. Auth0 is faster to onboard and you don’t run Postgres or the admin cluster yourself. Either works.
Group-to-role mapping, the actual logic
Here’s where the federated identity gets paid back. Each downstream component reads the same group claim and maps it to local roles.
| Platform | Citizen marketing | Citizen finance | Citizen sales |
|---|---|---|---|
| n8n | Editor on marketing-* workflows |
Editor on finance-* |
Editor on sales-* |
| Make | Team Marketing |
Team Finance |
Team Sales |
| Kong (citizen gateway) | ACL citizen-marketing |
citizen-finance |
citizen-sales |
| Internal connector library npm scope | Read | Read | Read |
The point of the table is that every column is provisioned the same way — group membership in the IdP. Adding a new business unit means one Keycloak group, one set of ACLs, one set of platform team mappings. Removing a person means one disable in the IdP.
Connector library access feeds back into the reusable connectors post — group membership is what gates which connectors a citizen dev can install in their workflow.
MFA, mandatory
I shouldn’t have to say this, but MFA is mandatory for citizen developers. They are operating workflows that touch your business data. The IdP enforces MFA, the platform never sees a password.
In Keycloak 25 — set the realm’s otpPolicyType and require OTP at registration. WebAuthn is also supported and is what I’d default to for new programs.
In Auth0 — enable MFA in the dashboard, require it for all users, support both OTP and WebAuthn factors.
Either way, the platform admin UI (n8n’s editor, Make’s scenario builder) inherits the MFA enforcement automatically because authentication happens at the IdP, not the platform.
Service identities for workflow runs
Citizen workflows that run on a schedule need to act as someone. That someone is not the citizen dev who built it. It’s a service identity owned by the workflow.
The pattern:
- Each workflow gets a service principal in the IdP, named
svc-wf-{workflow-id}. - The service principal inherits group membership from the owning business unit’s “service” group, not the citizen dev’s group.
- Workflow credentials reference this service principal, not the citizen dev’s user.
- When the citizen dev leaves, their user disables; the workflow keeps running because the service principal is independent.
This decouples human lifecycle from workflow lifecycle, which is what you want. The trick is making it the default — the connector library should provision a service principal at workflow creation, not at the first error.
Common pitfalls
Where this goes wrong.
- Local accounts as “just for the admin.” The admin account becomes the shared service account, the password lives in 1Password, somebody DMs it to someone else, three years later you have no idea who has it. Disable local auth entirely once SSO is up.
- No claims-to-role mapping documented. The team that wired the SSO knows that group X equals role Y. Nobody else does. Write it down. Put it in your platform README.
- MFA opt-in. “We’ll let people enable it themselves.” Half of them won’t. Enforce it.
- Forgetting service identities. Workflows tied to citizen dev users go dark when those users leave. Always provision a service principal.
- Skipping group claim validation on the proxy. I’ve seen
oauth2-proxyconfigs withoutallowed_groups. Then any authenticated user, even from a completely different realm, can hit n8n. Always enforce group membership at the proxy. - Long token lifetimes. Access tokens for citizen dev sessions should be short (15 min) with refresh-token rotation. Long-lived tokens that get cached on shared machines are a leak.
- Different realms with different rules. If your engineering realm has WebAuthn and your citizen-devs realm has password-only, you have a soft underbelly. Match policies or justify the gap explicitly.
Wrapping up
Identity federation is one of those things that feels like infrastructure overhead until the first time someone leaves the company and you realise you can’t audit what they touched. Then it becomes the cheapest thing you ever built.
The mechanics are not exotic. Keycloak 25 or Auth0 in front, OIDC or SAML to your low-code platforms, group claims doing the access mapping, MFA enforced at the IdP. Once it’s in place, every subsequent platform integration is cheaper because the identity story is settled.
Next post in the series, the decision matrix for when pro-code wins over low-code. After several weeks of “here’s how to enable citizen developers,” the counter-balance piece — here’s when you should refuse to let them ship something and write it yourself instead. See you Monday.