Migrating from Keycloak, Auth0, Okta or Cognito
Docs version v1 (main)Nauthera is pre-1.0, in a design-partner programUpdated
This guide moves applications and their users from another OpenID Connect provider, such as Keycloak, Auth0, Okta, Cognito or an in-house server, to Nauthera. It assumes you move one application at a time, with both providers running side by side until the last one has moved. If you are keeping your provider upstream instead, read Existing Identity Providers first: in that pattern no accounts move at all.
What carries over and what does not
| Carries over | Changes | |
|---|---|---|
| Client IDs | Yes. spec.clientId is yours to choose, so an application can keep the ID it has. | |
| Redirect and logout URIs | Yes, listed in the OIDCClient: redirect, post-logout and front-channel logout URIs. | A back-channel logout URI is registered through dynamic registration; an OIDCClient uses front-channel logout. |
| Issuer | Always. Every application and API is reconfigured with Nauthera's issuer, discovery URL and JWKS. | |
| Client secrets | Always. The server generates secrets and you cannot set one. A client that already authenticates with private_key_jwt keeps its key and only re-registers its jwksUri. | |
| User accounts | Email, username, a verified address, and string attributes such as name | Each account gets a new id in Nauthera, which is the new sub. See Keeping user identifiers stable. |
| Passwords | Not importable as hashes. Users set a new password, accept an invitation, or sign in through a federated provider. | |
| Second factors and passkeys | Re-enrolled. TOTP seeds and passkeys are not imported, and passkeys are bound to the new sign-in host anyway. | |
| Sessions and refresh tokens | Users sign in once more. The old provider's refresh tokens do not work at Nauthera. | |
| Consent | Asked again, where a client asks for consent. | |
| Groups and roles in tokens | Nauthera's tokens carry scopes and its own admin roles, not the old provider's groups. Map what your APIs check to scopes. |
1. Take an inventory
Before any change, list per application:
- the client ID, grant types, redirect URIs, post-logout URIs, scopes and how it authenticates to the token endpoint;
- which claims it reads, and which of them identify the user (
sub,email, a custom id claim); - whether it uses refresh tokens, back-channel or front-channel logout, or a provider SDK with features beyond OpenID Connect (user management calls, hosted pages, rules or actions that add claims);
- the APIs it calls with access tokens, and what those APIs check (
aud, scopes, groups); - its owner, and when a sign-in outage would hurt least.
Anything that relies on SAML, a provider-specific API, or claims Nauthera does not issue needs a decision before it moves. See What Nauthera does not cover.
2. Register the clients
Write one OIDCClient per application, keeping its client ID.
Tighten what you can while you are at it: PKCE is enforced by default, and
private_key_jwt removes the secret altogether.
apiVersion: nauthera.io/v1alpha1
kind: OIDCClient
metadata:
name: billing-web
namespace: billing # a namespace the staff organization admits
spec:
clientId: billing-web # the same ID as at the old provider
redirectUris: [https://billing.example.com/callback]
postLogoutRedirectUris: [https://billing.example.com/]
grantTypes: [authorization_code, refresh_token]
scopes: [openid, profile, email, offline_access]
allowRefresh: true
secret:
generate: trueThe new secret lands in the Secret the operator writes beside the application. Keeping
the client ID means the application's own records that mention it, and the logs your
teams search, keep meaning the same thing.
Outside Kubernetes, nauthera-cli create client registers the same thing (see
nauthera-cli).
3. Move the APIs first
An API that accepts tokens from both issuers lets its callers move one by one. Configure each API with two trusted issuers, each with its own JWKS and audience, until the last caller has moved:
- accept a token whose
issis the old provider's, checked against the old JWKS, as today; - accept a token whose
issis Nauthera's, checked against Nauthera's JWKS, withaudthe API's resource URI andtypat+jwt.
Never pick the key set by the token's unverified iss alone against a list you don't
control; list both issuers explicitly. Most JWT libraries take several issuers or
several validation configurations (see the framework guides).
4. Move the users
Users
Choose per organization how accounts arrive. You can combine them.
| Approach | How | The user sees |
|---|---|---|
| Invite | Create each account with "invite": true. Needs an email relay. | An email naming the administrator, with a link to set a password. Valid for auth.invitation.validity (default 7 days, at most 30). |
| Create, then reset | Create each account with a random password you discard, and email_verified: true. | At first sign-in, Forgot password, an emailed link, and a new password. |
| Federate | Keep the old provider, or the company directory, as an upstream IdentityProvider with trustEmail. Pre-created accounts with a verified address are linked on first sign-in. | The provider's button, and nothing new to remember. |
| Sign up | Let users create their own accounts (auth., server-wide). | A sign-up form. Suits customer apps where the old accounts were mostly inactive. |
Moving accounts is a loop over the admin API:
# users.jsonl: one {"email", "username", "name", "old_sub"} per line, exported from the old provider
while read -r u; do
body=$(jq -c '{email, username, invite: true, locale: "en",
email_verified: true, attributes: {name, old_sub}}' <<<"$u")
curl -sf https://auth.example.com/v1/admin/orgs/staff/users \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "$body" | jq -c --arg old "$(jq -r .old_sub <<<"$u")" '{old_sub: $old, new_sub: .id}'
done < users.jsonl > sub-map.jsonl- Use a token of the organization's
{slug}-admin-m2mclient, which holdsorg-user-admin. Its secret is theadmin_m2m_secretthatPOST /v1/admin/orgsreturned once (Getting a token). For an organization declared as anOrganizationresource, whose m2m secret nobody holds, use the token of an administrator who holdsorg-user-adminthere. Creating users needs no recent sign-in. - Emails are unique per organization, so a second run refuses the accounts the first created. Make the script resumable.
- The admin API is rate limited like every endpoint (
rate_limit.*); pace a large import, or raise the limit for its duration. - Import suspended and blocked accounts too, with a random
passwordin place ofinvite: trueso no invitation goes out. Then an administrator suspends or blocks each one (POST /orv1/ admin/ orgs/ {slug}/ users/ {id}/ suspend …/block, with areason). Those calls need a recent sign-in, so a person makes them, not the m2m client. attributesare strings, and thenameattribute becomes thenameclaim. Keep any other attribute you may need, such as the oldsub, there too.
Second factors
TOTP and passkeys are enrolled again. With a second factor required by the organization's policy, each user is taken through enrolment at their first sign-in, and signing up or accepting an invitation is never a way around it. Tell users before their wave, and make sure the support team knows how to help someone who lost their factor.
Keeping user identifiers stable
Applications that store the old provider's sub as the user's key will see a new one.
Pick one of these per application, before it moves:
- Map it once. The import above writes
sub-map.jsonl, oldsubto new. Rewrite the application's user table with it during the cutover. - Map it on first sign-in. On a sign-in with an unknown
sub, find the user by verifiedemail(email_verifiedtrue), store the newsubbeside the old one, and from then on look users up by the new one. Only safe when addresses are verified on both sides. - Use an application-owned id. If the application already keys users by its own id
and keeps the provider's
subas an attribute, it only adds the new one.
5. Cut over one application
- Deploy the application with Nauthera's issuer, the new client secret or key, and the scopes it needs, behind a switch you can flip back.
- Flip it. New sign-ins go to Nauthera. Sessions at the old provider are not carried over; users sign in once more.
- Watch the sign-in audit events and the application's errors for the first hours. The client's daily users in the dashboard should climb to what the old provider showed.
- Leave the old client registered until its longest-lived refresh token has expired, so flipping back stays possible.
Rollout covers waves, go/no-go criteria and rollback in detail.
Retiring the old provider
When every application has moved:
- remove the second issuer from every API;
- disable, then delete, each client at the old provider once its sign-ins have dropped to zero;
- export what you must keep from it, such as its audit or sign-in logs, under your retention rules: Nauthera's audit log starts on the day of the first sign-in through it;
- if the old provider stays as an upstream
IdentityProvider, keep its registration of Nauthera, and nothing else.