Skip to content

The v1 docs follow nauthera-server's main branch and its 0.x releases on the way to 1.0, so fields and APIs can still change.

v1

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 overChanges
Client IDsYes. spec.clientId is yours to choose, so an application can keep the ID it has.
Redirect and logout URIsYes, 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.
IssuerAlways. Every application and API is reconfigured with Nauthera's issuer, discovery URL and JWKS.
Client secretsAlways. 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 accountsEmail, username, a verified address, and string attributes such as nameEach account gets a new id in Nauthera, which is the new sub. See Keeping user identifiers stable.
PasswordsNot importable as hashes. Users set a new password, accept an invitation, or sign in through a federated provider.
Second factors and passkeysRe-enrolled. TOTP seeds and passkeys are not imported, and passkeys are bound to the new sign-in host anyway.
Sessions and refresh tokensUsers sign in once more. The old provider's refresh tokens do not work at Nauthera.
ConsentAsked again, where a client asks for consent.
Groups and roles in tokensNauthera'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: true

The 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 iss is the old provider's, checked against the old JWKS, as today;
  • accept a token whose iss is Nauthera's, checked against Nauthera's JWKS, with aud the API's resource URI and typ at+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.

ApproachHowThe user sees
InviteCreate 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 resetCreate 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.
FederateKeep 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 upLet users create their own accounts (auth.registration.enabled, 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-m2m client, which holds org-user-admin. Its secret is the admin_m2m_secret that POST /v1/admin/orgs returned once (Getting a token). For an organization declared as an Organization resource, whose m2m secret nobody holds, use the token of an administrator who holds org-user-admin there. 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 password in place of invite: true so no invitation goes out. Then an administrator suspends or blocks each one (POST /v1/admin/orgs/{slug}/users/{id}/suspend or …/block, with a reason). Those calls need a recent sign-in, so a person makes them, not the m2m client.
  • attributes are strings, and the name attribute becomes the name claim. Keep any other attribute you may need, such as the old sub, 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, old sub to 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 verified email (email_verified true), store the new sub beside 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 sub as an attribute, it only adds the new one.

5. Cut over one application

  1. 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.
  2. Flip it. New sign-ins go to Nauthera. Sessions at the old provider are not carried over; users sign in once more.
  3. 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.
  4. 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.