Skip to main content
Jeff Patzer
Phase Two

Jeff is the co-founder and COO of Phase Two, and has been actively contributing to Keycloak and its open-source extension ecosystem for many years. Prior to Phase Two, he built a successful career as an engineering director at a major CDN.

View all authors

Keycloak slow migration vs fast migration: moving your users

· 11 min read
Jeff Patzer
Phase Two

A slow migration moves users from a legacy system into Keycloak one at a time, as each of them logs in. It goes by other names too: lazy migration, just-in-time (JIT) migration, trickle migration. Nobody gets a password reset email, there is no cutover weekend, and users who never come back are never migrated.

The other side is a fast migration: a bulk import that moves every user into Keycloak at once, before or at cutover. It is quicker to finish and easier to reason about, but it needs something a slow migration doesn't, usually the users' password hashes.

Keycloak doesn't ship slow migration. You add it with a user storage provider, and the one almost everyone uses is open source on GitHub: daniel-frak/keycloak-user-migration. This post covers how slow and fast migrations work, where the code lives, how to choose, and how to combine the two so a slow migration doesn't drag on for months.

Slow migrationFast migration
When users moveOne at a time, at their next loginAll at once, at import
Password hashes neededNo, the legacy system checks passwordsYes, or users set a new password
Legacy system after cutoverMust stay up and answer the migration endpointsCan be turned off
Dormant usersNever migrated unless you add a final stepMigrated with everyone else
Time to finishWeeks to months, plus a long tailHours to days

Passkeys make login faster. Here is the data, and how to roll them out in Keycloak

· 12 min read
Jeff Patzer
Phase Two

Every second a customer spends on your login page is a second they might spend somewhere else. Typing an email and password, mistyping it, waiting for a code, switching to an authenticator app, or giving up and resetting the password are all time between someone deciding to sign in and actually getting in. Passkeys remove most of those steps, and every large published measurement shows the same result: sign-in with a passkey takes about half the time, or less, and fails far less often. Our own customers report the same: after introducing passkeys, they measured login times drop by several seconds per sign-in.

This post collects the published figures, explains where the time goes, and shows how to introduce passkeys in Keycloak alongside the password login you already have.

Replacing homegrown e-commerce login with Keycloak

· 11 min read
Jeff Patzer
Phase Two

The login page is the most attacked page on an online store, and it is rarely the one anyone designed for attack. Homegrown e-commerce login usually starts as a users table, a bcrypt column and a session cookie, and it grows a password reset, a mobile API, a trade-account flag and a social button over the years. What it almost never grows is a defense against credential stuffing: automated replay of email and password pairs leaked from other sites, aimed at whichever of your customers reused one. Replacing homegrown login with Keycloak gives a store one identity service for web, app and back office, with social login, B2B accounts and branded pages built in, and you can move customers onto it without asking anyone to reset a password. It does not make the login page safe on its own. The defaults leave doors open that attackers look for first, and the rest of this post covers both halves: what you gain, and what has to be layered on top.

"Restart login cookie not found" in Keycloak

· 9 min read
Jeff Patzer
Phase Two

"Restart login cookie not found. It may have expired; it may have been deleted or cookies are disabled in your browser." Keycloak shows this on a dead-end HTTP 400 page, and the message is more confident than it should be. It means exactly one thing: the request arrived with no KC_RESTART cookie at all. Not a stale one, not a rejected one. None. That cookie has no expiry to run out, so "it may have expired" is the one explanation that is never true. A rejected cookie produces a different message. And the page tells the user to "Click Back to Application", which on a default client renders no link to click.

Here is what the cookie is for, how to tell in one log line which failure you have, and the one client field that turns the dead end into a recoverable page.

Where your Keycloak data lives: Frankfurt, Ireland, and what leaves the EU

· 8 min read
Jeff Patzer
Phase Two

Here is where your Keycloak data lives with us. The Keycloak database, sessions, event store, backups and realm signing keys for an EU cluster run in AWS Frankfurt (eu-central-1), with backups in Ireland (eu-west-1), and are never replicated outside the EU. Three things of ours do run in the United States: the dashboard you sign in to, support tickets, and billing. You contract with Phase Two UK, Ltd, registered in England and Wales, company number 16815578. Our parent company, Phase Two, Inc., is incorporated in the United States, and our control plane runs there, so administrators outside the EU can reach your cluster in order to operate it. Under Chapter V of the GDPR that access is a restricted transfer, and we would rather write it down here than have you find it in a risk assessment.

Keycloak Meetup #1: London, 20 November

· 4 min read
Jeff Patzer
Phase Two

The Keycloak User Group UK is holding its first meetup on Friday 20 November, 1–5pm, at London Bridge Hive — 8 Holyrood Street, London. Phase Two is sponsoring it, and we would like you there. It is a few short talks, some hands-on workshops, and an open discussion, followed by beers at Nine Lives from 5pm.

RSVP here. It is free, and the form is the only place that counts — see below.

Keycloak infinite redirect loop at login

· 8 min read
Jeff Patzer
Phase Two

A Keycloak infinite redirect loop at login almost always means one thing: a cookie Keycloak set on the way to the login page did not come back on the way out of it. Keycloak has no session to resume, so it starts the flow again, and the browser bounces between your application and /protocol/openid-connect/auth until it gives up.

The reason this is so common is in the Set-Cookie headers. Keycloak 26.8.0 marks every cookie in the login flow Secure; SameSite=None, unconditionally — there is no setting that relaxes it. A browser will not store a Secure cookie that arrives over plain http://, and it will not store SameSite=None without Secure. So the loop is usually not an OIDC problem at all. It is a transport problem that the OIDC layer reports as a lost session.

Migrating from Auth0 to Keycloak: a complete walkthrough

· 20 min read
Jeff Patzer
Phase Two

Migrating from Auth0 to Keycloak is three separate jobs wearing one name. Profiles move easily — Auth0's export job gives you newline-delimited JSON and Keycloak's partial-import endpoint takes it. Passwords do not move at all: Auth0 hashes with bcrypt, Keycloak 26.7.4 ships four password-hashing providers and none of them is bcrypt, so you either reset every password or stand up a bridge that verifies against the old hashes on first login. Your Rules and Actions have to be rewritten, because there is no equivalent runtime — some of them become authentication-flow configuration with no code at all, and some become a Java SPI.

The part that costs people a weekend is none of those. It is that Keycloak's admin API answers 201 Created to three different malformed migrations and then quietly loses the data. Everything below was run against Keycloak 26.7.4 on PostgreSQL 16.

Keycloak 403 Forbidden: client, realm and admin API causes

· 12 min read
Jeff Patzer
Phase Two

A Keycloak 403 Forbidden always means the same thing: your token was accepted, and the permissions attached to it were not enough. That is the whole difference from a 401, which means the token itself was rejected — missing, expired, malformed, or signed by another realm. Checking which of the two you have is the fastest useful thing you can do, because 401 sends you to the token and 403 sends you to role mappings, and they share no fixes.

After that, the question is which surface returned it. The Keycloak admin console, the Admin REST API and your own application all return 403 for unrelated reasons, and the response body tells you which one you are looking at without any further digging.

JWKS explained: rotation, caching, validating tokens

· 14 min read
Jeff Patzer
Phase Two

A JWKS — JSON Web Key Set — is a JSON document containing a list of public keys, each encoded as a JWK (JSON Web Key). Its purpose is to let anyone verifying a signed token fetch the right public key over HTTP instead of having it configured by hand. The token names the key it was signed with in its kid header; the verifier looks that kid up in the set.

That is the whole idea, and it buys one specific thing: the signer can change keys without anyone who verifies its tokens changing configuration. Everything else about JWKS — the field names, the caching, the rotation ordering — follows from that.

This post covers what is actually in a JWK, where the JWKS URL comes from, what Keycloak publishes and what it deliberately does not, and the two directions the traffic flows in. Every number and every error string below came from a real run.

Tested against

Keycloak 26.7.4 in a container, plus Python 3.12 with cryptography 46 for the key arithmetic. JWK field definitions are from RFC 7517 and RFC 7518.