# Keycloak and Auth0

> Don't build auth yourself: a managed identity provider (Auth0) or a self-hosted one (Keycloak) gives you login, social sign-on, MFA, and OIDC out of the box.


---

# Keycloak and Auth0

You need login. So you start a `users` table, a password hash, a reset-email flow, and three weeks later you are reading about timing attacks at midnight and still have no social sign-on, no MFA, no SSO for that enterprise customer who signed last week. The dread is correct: auth is a swamp that never stops asking for more.

Here is the relief. You do not have to live in that swamp. An identity provider is a finished product that does login, social sign-on, MFA, user management, and OIDC for you. You pick one - managed (Auth0) or self-hosted (Keycloak) - point your app at it, and get back to building the thing people actually pay for.

## How to read this

Go in order. Phase 1 is the mental model: why you outsource identity, and what an identity provider hands you the moment you adopt one. Phase 2 is the everyday core, taught through Keycloak's vocabulary - realms, clients, roles - because those words map onto every IdP once you know them. Phase 3 is the real decision: managed versus self-hosted, what each costs you, and the gotchas that bite in production.

This guide assumes you know roughly what OIDC is. If "ID token" and "redirect URI" are fuzzy, read /guides/oauth2-and-oidc first; the difference between login and permissions lives in /guides/auth-vs-authz.

## The phases

1. [Phase 1: Stop Building Auth](01-stop-building-auth.md) - why identity is a buy-or-host decision, and what an IdP gives you for free.
2. [Phase 2: Realms, Clients, and Roles](02-realms-clients-roles.md) - the everyday vocabulary, wiring an app, and where users and permissions live.
3. [Phase 3: Managed vs Self-Hosted, and the Gotchas](03-managed-vs-self-hosted.md) - Auth0 vs Keycloak in production, costs, lock-in, and what breaks.


---

# Stop Building Auth

You have done this before, or you are about to. A `users` table. A column for the password hash. A signup form, a login form, a "forgot password" email. It feels like a Tuesday-afternoon task. It is not.

## The swamp you are about to walk into

Write down everything "let users log in" actually contains:

```text
- password hashing (which algorithm? what cost factor?)
- password reset (email tokens, expiry, single-use)
- email verification
- rate limiting on login (or you invite credential stuffing)
- account lockout and the lockout-as-DoS problem it creates
- session management and cookie security
- "Log in with Google / GitHub / Microsoft"
- multi-factor auth (TOTP apps, SMS, passkeys)
- the enterprise customer who demands SAML SSO
- an admin screen to find a user and reset their access
- audit logs for "who logged in, from where, when"
- GDPR-style account deletion and data export
```

*What just happened:* the line item "login" exploded into a dozen sub-projects, each with its own security failure mode. Every one of these is a place an attacker probes, and getting any single one wrong can hand over accounts.

This is the core insight of the whole guide: **identity is not a feature of your app, it is a product in its own right** - one that other people have already built, hardened over years, and been attacked through so you don't have to be.

> The cost of getting auth wrong is not a bug ticket. It is an account takeover, a breach disclosure, and a customer who never comes back. That asymmetry is why "buy or host, don't build" is the default for everyone who has been burned once.

## What an identity provider actually is

An **identity provider** (IdP) is a separate service whose entire job is to answer one question for your app: *who is this person, and are they who they claim to be?* You hand off login to it and get back a signed token that says "this is user 8842, email asha@example.com, verified." Your app trusts the token, not a password it stored.

```text
        ┌─────────┐   1. "log me in"    ┌──────────────┐
 user → │ your app │ ──────────────────→ │ identity     │
        │          │ ←────────────────── │ provider     │
        └─────────┘   2. signed token    │ (Auth0 /     │
            │  3. trust the token,        │  Keycloak)   │
            ▼     never see the password  └──────────────┘
        your business logic
```

*What just happened:* your app never touches the password. The IdP runs the login screen, checks credentials (or a Google account, or a passkey), and returns a token your app verifies with a public key. Your codebase shrinks to "validate this token, read the user out of it."

The protocol underneath that token exchange is OIDC - the same authorization-code flow covered in /guides/oauth2-and-oidc. An IdP is, in one sentence, a polished product wrapped around an OIDC server plus a user database plus an admin console.

## What you get the day you adopt one

The same checklist from the swamp, except now it ships in the box:

```text
✓ login + signup UI (hosted, branded to look like yours)
✓ password reset + email verification flows
✓ social login: Google, GitHub, Microsoft, Apple, ...
✓ MFA: TOTP authenticator apps, SMS, passkeys/WebAuthn
✓ SSO: SAML and OIDC for enterprise customers
✓ user management console (find, edit, disable, reset)
✓ RBAC: define roles, assign them, read them in the token
✓ audit logs, brute-force protection, session control
✓ standards-based tokens any language can verify
```

*What just happened:* the three-week project from the intro collapsed into a configuration task. You are no longer writing security-critical code; you are turning features on.

## The two doors: managed and self-hosted

There are two shapes of IdP, and the whole rest of this guide hangs on the difference:

- **Auth0** - *managed.* A company runs the servers; you sign up, configure through a dashboard, and pay per active user. Zero ops, fastest to a working login, priced by usage.
- **Keycloak** - *self-hosted, open source.* You run the server (a Java application, usually in a container) on your own infrastructure. No per-user fee, full control of the data, and you own the upgrades, backups, and uptime.

```text
 MANAGED (Auth0)              SELF-HOSTED (Keycloak)
 ─────────────────           ──────────────────────
 someone else runs it        you run it
 pay per active user         pay for the servers + your time
 ops handled for you         you own upgrades & backups
 data lives in their cloud   data lives where you put it
 fastest to "it works"       most control, no vendor lock-in
```

*What just happened:* you saw the trade in one frame. Managed buys you time with money and a vendor relationship; self-hosted buys you control and data ownership with operational work. Phase 3 turns this into an actual decision.

The good news: both speak the same protocols. Whichever door you walk through, your app verifies an OIDC token the same way. The IdP is replaceable; the standard is not.

## For builders

Reach for an IdP the moment auth stops being trivial - the first time someone asks for "Log in with Google," for MFA, or for SSO. Even a weekend project benefits, because the hosted login screen and password-reset flow alone save you the riskiest code you'd otherwise write. The rare case for building it yourself is when your auth requirements are genuinely strange and tiny, and even then most teams regret it by the second feature request.

```quiz
[
  {
    "q": "What is the core mental-model claim of this phase?",
    "choices": [
      "Identity is a small feature you should hand-roll to save money",
      "Identity is a product in its own right, already built and hardened by others",
      "Auth0 and Keycloak are databases you query directly",
      "OIDC replaces the need for any identity provider"
    ],
    "answer": 1,
    "explain": "Auth ('login') explodes into a dozen security-critical sub-projects, so you adopt a finished identity product rather than rebuild it."
  },
  {
    "q": "After adopting an identity provider, what does your app store and check?",
    "choices": [
      "The user's raw password, hashed with bcrypt",
      "Nothing about the user; it only logs requests",
      "A signed token from the IdP, which it verifies rather than the password",
      "A session row keyed by the user's SMS code"
    ],
    "answer": 2,
    "explain": "The IdP runs login and returns a signed token; your app verifies the token with a public key and never touches the password."
  },
  {
    "q": "Which statement correctly contrasts Auth0 and Keycloak?",
    "choices": [
      "Auth0 is open source and self-hosted; Keycloak is managed and paid",
      "Auth0 is managed and priced per user; Keycloak is self-hosted and open source",
      "Both are managed services with no self-hosting option",
      "Both require you to implement OIDC yourself"
    ],
    "answer": 1,
    "explain": "Auth0 is a managed, pay-per-active-user service; Keycloak is open source and runs on your own infrastructure."
  }
]
```


---

# Realms, Clients, and Roles

Open any identity provider for the first time and the dashboard throws nouns at you: realms, clients, tenants, applications, roles, scopes, mappers. It feels like learning a new language before you can do one useful thing. So learn the three words that carry the weight. We'll use Keycloak's names because they're the most explicit; Auth0 has the same ideas under slightly different labels, noted as we go.

## Realm: the walled garden of users

A **realm** is a self-contained universe of users, credentials, roles, and settings. Users in one realm cannot see or log into another. Everything you configure - login policy, social providers, MFA rules - lives inside a realm.

```text
Keycloak instance
├── realm: acme-staff        ← your employees
│     users, roles, login policy, MFA
├── realm: acme-customers    ← the people who buy from you
│     users, social login, self-signup
└── realm: master            ← admin-only; do NOT put app users here
```

*What just happened:* you separated two completely different populations. Staff and customers never mix, have different login rules, and can't authenticate across the boundary. The `master` realm exists only to administer Keycloak itself - putting application users there is a classic first mistake.

In Auth0 the equivalent boundary is the **tenant** (one per environment, e.g. `acme-dev`, `acme-prod`), with **connections** carrying the user populations inside it. Different word, same job: an isolated container of identities.

## Client: one app that trusts the realm

A **client** is a registration for one application that wants to use the realm to log people in. Your web frontend is a client. Your mobile app is another client. Your backend API is a third. Each gets its own ID and its own settings.

```text
realm: acme-customers
├── client: web-spa
│     type: public (no secret - runs in a browser)
│     redirect URIs: https://app.acme.com/callback
│     flow: authorization code + PKCE
├── client: mobile-app
│     type: public
│     redirect URIs: com.acme.app://callback
└── client: orders-api
      type: confidential (has a secret - runs on a server)
      used to validate incoming tokens
```

*What just happened:* each app declared who it is and where the IdP is allowed to send users back after login. That **redirect URI** is a security control, not a convenience: the IdP refuses to return a token to any URL not on the list, which is what stops an attacker from stealing the login response.

Two client types matter:

- **public** - runs somewhere a secret can't be hidden (a browser SPA, a mobile app). It proves itself with PKCE instead of a secret.
- **confidential** - runs on a server where a secret stays secret. It can use a client secret to authenticate.

Auth0 calls a client an **application**, with the same public/confidential split (it labels them "Single Page App," "Native," "Regular Web App," "Machine to Machine"). The mental model is identical.

## Wiring an app: the smallest real config

To connect an app you need three values from the realm and one decision. Here is what a frontend's config actually looks like:

```yaml
# what your app needs to talk to the IdP
issuer:    https://id.acme.com/realms/acme-customers
client_id: web-spa
redirect_uri: https://app.acme.com/callback
# no client_secret - this is a public client using PKCE
```

*What just happened:* `issuer` points at the realm, `client_id` names your registered client, and `redirect_uri` matches one you allow-listed. From the issuer URL the app can discover everything else automatically - every OIDC provider publishes its endpoints and signing keys at a well-known address:

```bash
curl https://id.acme.com/realms/acme-customers/.well-known/openid-configuration
```

```json
{
  "issuer": "https://id.acme.com/realms/acme-customers",
  "authorization_endpoint": "https://id.acme.com/realms/acme-customers/protocol/openid-connect/auth",
  "token_endpoint": "https://id.acme.com/realms/acme-customers/protocol/openid-connect/token",
  "jwks_uri": "https://id.acme.com/realms/acme-customers/protocol/openid-connect/certs",
  "userinfo_endpoint": "...",
  "end_session_endpoint": "..."
}
```

*What just happened:* you fetched the realm's public directory. Your auth library reads this once and knows where to send the user to log in, where to exchange the code for a token, and (via `jwks_uri`) which public keys verify the token's signature. You configured one URL; the standard filled in the rest. This is why swapping Auth0 for Keycloak later is mostly a change of issuer URL - both publish the same well-known document.

## Roles: who is allowed to do what

Login tells you *who* the user is. **Roles** are how the realm records *what they may do* - the authorization half of the story (the full distinction lives in /guides/auth-vs-authz). You define roles in the realm, assign them to users, and the IdP stamps them into the token.

```text
realm roles:  admin, editor, viewer

user: asha@example.com  →  roles: [editor]
user: ops@example.com   →  roles: [admin]
```

When `asha` logs in, her token carries her roles. Decoded, the relevant slice looks like this:

```json
{
  "sub": "8842-asha",
  "email": "asha@example.com",
  "realm_access": {
    "roles": ["editor"]
  },
  "exp": 1751299200
}
```

*What just happened:* your API doesn't query a database to learn Asha is an editor - it reads `realm_access.roles` straight out of the verified token. The IdP is the single source of truth for both identity and roles, and your app trusts the signature. (Auth0 delivers the same thing through roles/permissions surfaced as custom claims in the token.)

> Keep roles coarse - `admin`, `editor`, `viewer` - and decide fine-grained, data-specific permissions ("can edit *this* document") in your own app. The IdP knows roles; it does not know your business objects. Cramming per-record rules into the token bloats it and couples your domain to your auth vendor.

## The everyday loop

Day to day, working with an IdP looks like this, almost entirely through its admin console:

```text
1. create the realm/tenant once per environment
2. register a client/application per app
3. allow-list its redirect URIs
4. turn on the login methods you want (password, Google, MFA)
5. define a few roles
6. point your app at the issuer + client_id
7. let users sign up; assign roles as needed
```

*What just happened:* notice how little of this is code. Steps 1–5 are configuration in a UI (or a config file you commit, covered next phase). Step 6 is a handful of lines in your app. The IdP did the hard part.

## In the wild

A common production shape: one realm for customers with self-signup and social login switched on, a second realm for internal staff locked down with mandatory MFA and no self-signup, and each microservice registered as its own confidential client validating tokens against the realm's `jwks_uri`. Same instance, two walled gardens, every service trusting the same signed tokens.

```quiz
[
  {
    "q": "In Keycloak, what is a realm?",
    "choices": [
      "A single application registered to use login",
      "A self-contained set of users, roles, and settings, isolated from other realms",
      "A role assigned to an administrator",
      "The secret a confidential client uses to authenticate"
    ],
    "answer": 1,
    "explain": "A realm is an isolated universe of users and configuration; users in one realm cannot log into another. Auth0's equivalent is a tenant."
  },
  {
    "q": "Why does a client declare allow-listed redirect URIs?",
    "choices": [
      "To make the login page load faster",
      "So the IdP refuses to return a token to any URL not on the list, blocking token theft",
      "Because OIDC requires exactly one URI per realm",
      "To store the user's roles"
    ],
    "answer": 1,
    "explain": "The redirect URI is a security control: the IdP only sends the login response to pre-registered URLs, which stops attackers from capturing it."
  },
  {
    "q": "How does your API learn that a user has the 'editor' role?",
    "choices": [
      "It queries the realm database on every request",
      "It reads the roles claim out of the verified token the IdP issued",
      "It asks the user to re-enter their password",
      "Roles are not available to APIs, only to the login page"
    ],
    "answer": 1,
    "explain": "The IdP stamps roles into the token (e.g. realm_access.roles); the API trusts the signature and reads them directly, no extra lookup."
  }
]
```


---

# Managed vs Self-Hosted, and the Gotchas

By now the question is no longer "should I use an IdP" but "which one, and what will it cost me later." This is where the calm decision matters, because both choices are easy to start and expensive to reverse once thousands of users live inside.

## The real trade: where the work goes

Both options remove the security-critical code from your app. What differs is where the remaining work and the bill land.

```text
                    Auth0 (managed)        Keycloak (self-hosted)
 who runs it        the vendor             you
 the bill scales    per active user        per server (flat-ish)
 ops burden         near zero              real: upgrades, HA, backups
 data residency     vendor's cloud         wherever you host
 time to first login fastest               slower (stand up a server)
 lock-in            higher                 lower (open source, OIDC std)
 outage = your      vendor's status page   your pager
```

*What just happened:* the table reframes the choice as "rent versus own." Auth0 converts auth into a predictable line item and someone else's pager. Keycloak converts it into infrastructure you control and operate. Neither is wrong; they fail differently.

## When managed (Auth0) makes sense

Reach for managed when your scarce resource is *time and people*, not money:

```text
- small team, no one who wants to own an identity server
- you need login working this week, not this quarter
- user counts are modest, or revenue-per-user is healthy
- you have no hard data-residency / on-prem requirement
- you'd rather page a vendor than yourself at 3am
```

*What just happened:* you matched the tool to a team that values shipping over control. The classic regret here is the bill: per-active-user pricing is gentle at 500 users and a board-meeting topic at 500,000. Model the cost at your *target* scale, not today's.

## When self-hosted (Keycloak) makes sense

Reach for self-hosted when control, cost-at-scale, or data location dominate:

```text
- data must stay in your cloud / region / on-prem (compliance)
- very large or fast-growing user base (per-user pricing hurts)
- you already run infrastructure and ops is a muscle you have
- you want zero vendor lock-in (it's open source + standards)
- you need deep customization of the auth flows
```

*What just happened:* you matched the tool to a team that can absorb operational work in exchange for control and a flatter cost curve. The classic regret here is underestimating ops: Keycloak is a stateful Java service backed by a database, and *you* now own its upgrades, high availability, and backups.

## Self-hosted reality: config as code

The professional way to run Keycloak is not clicking in the admin UI in production - it's exporting realm configuration to a file you commit, so environments are reproducible:

```bash
# export a realm to a JSON file you can version-control
kc.sh export --dir /tmp/export --realm acme-customers
```

```bash
# import it when standing up a fresh instance
kc.sh import --dir /tmp/export
```

*What just happened:* your realm - clients, roles, login settings - became a reviewable artifact in git, not a pile of manual clicks someone has to remember. This is the difference between "we have an auth server" and "we can rebuild our auth server." Auth0 has the same discipline via its CLI/Terraform provider; whichever you pick, config-as-code is the line between hobby and production.

## The gotchas that bite everyone

These cut across both Auth0 and Keycloak. Each has wrecked a real launch.

```text
- token lifetime: too long = a stolen token is valid for hours.
  Keep access tokens short; rely on refresh tokens to renew.
- the master/management realm: never put app users in it, and
  lock down its admin account hard - it owns everything.
- redirect URI sloppiness: a wildcard or a forgotten dev URL
  on the allow-list is an open door for token theft.
- clock skew: token validation checks 'exp'; if your server's
  clock drifts, valid tokens get rejected. Sync your clocks (NTP).
- HTTPS everywhere: tokens in transit over plain HTTP are
  credentials in plaintext. Keycloak refuses non-HTTPS by default
  outside localhost for exactly this reason.
- Keycloak upgrades: it's stateful with a DB schema. Read the
  upgrade notes, back up the database, and test before prod.
```

*What just happened:* every item is a place where a default left alone leaks accounts. Auth handed off is not auth ignored - you still own the configuration choices, and these are the ones that matter.

> The cardinal rule survives the handoff: do not roll your own. Adopting an IdP and then reaching back into its token signing, password hashing, or flow internals to "improve" them rebuilds the swamp you escaped. Configure it; don't reinvent it.

## A migration note, so you don't get trapped

Because both speak OIDC, moving between them is *possible* but not free. The protocol-facing parts of your app (token validation, the issuer URL) port cleanly. What doesn't port automatically: password hashes (you may need a gradual "rehash on next login" migration), vendor-specific extensions, and the exact shape of custom claims. Plan migrations as a project, not a config swap - and lean on standards (OIDC, standard claims) over vendor extensions so the door stays open.

## In the wild

A frequent path: a startup launches on Auth0 to get to market fast, runs happily for a couple of years, then watches the per-user bill cross the cost of an engineer and migrates to self-hosted Keycloak. Because they kept to standard OIDC and standard claims, the app code barely changed; the work was operational - stand up Keycloak, migrate users, cut over the issuer URL. The teams that suffer are the ones who leaned hard on one vendor's proprietary features and found the exit welded shut.

```quiz
[
  {
    "q": "What is the central trade-off between Auth0 and Keycloak?",
    "choices": [
      "Auth0 is less secure than Keycloak",
      "Managed trades money and some lock-in for near-zero ops; self-hosted trades operational work for control and a flatter cost curve",
      "Keycloak does not support OIDC",
      "Only Auth0 can do social login and MFA"
    ],
    "answer": 1,
    "explain": "Both remove security-critical code; the difference is where the work and bill land - rent (Auth0) versus own (Keycloak)."
  },
  {
    "q": "Why export a Keycloak realm to a committed file?",
    "choices": [
      "It encrypts the user passwords",
      "It makes realm config a versioned, reviewable artifact so environments are reproducible",
      "It is the only way to enable social login",
      "It disables HTTPS for local testing"
    ],
    "answer": 1,
    "explain": "Config-as-code (export/import to git) turns manual clicks into a reproducible artifact - the line between hobby and production."
  },
  {
    "q": "Which is a genuine production gotcha that applies to both providers?",
    "choices": [
      "Putting application users in the master/management realm",
      "Using HTTPS for token transport",
      "Keeping access-token lifetimes short",
      "Storing realm config in version control"
    ],
    "answer": 0,
    "explain": "App users belong in their own realm; the master realm administers the server itself and must be locked down. The other three are the correct practices."
  }
]
```
