# Migrating from Keycloak, Auth0, Okta or Cognito

> Moving apps and users to Nauthera from Keycloak, Auth0, Okta or Cognito: inventory, client and API cutover, accounts without password hashes and stable IDs.

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](https://www.nauthera.io/docs/v1/adoption/existing-identity-providers) first: in
that pattern no accounts move at all.

```mermaid
flowchart LR
  I[Inventory] --> C[Clients] --> A[APIs] --> U[Users] --> X[Cutover] --> R[Retire]
```

## 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](https://www.nauthera.io/docs/v1/sessions#telling-the-applications); 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](#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](https://www.nauthera.io/docs/v1/adoption#what-nauthera-does-not-cover).

## 2. Register the clients

Write one [`OIDCClient`](https://www.nauthera.io/docs/v1/oidc-client) 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.

```yaml
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](https://www.nauthera.io/docs/v1/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`.

```mermaid
flowchart LR
  M["App, moved"] -->|"Nauthera token"| API["API<br/>trusts both issuers"]
  O["App, not moved yet"] -->|"old provider's token"| API
```

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](https://www.nauthera.io/docs/v1/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](https://www.nauthera.io/docs/v1/email). | 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`](https://www.nauthera.io/docs/v1/identity-provider-resource) 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.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](https://www.nauthera.io/docs/v1/user-management#users):

```bash
# 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](https://www.nauthera.io/docs/v1/user-management#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](https://www.nauthera.io/docs/v1/mfa), 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.

```mermaid
flowchart TD
  T["Sign-in with an<br/>unknown sub"] --> V["email_verified?"]
  V -.->|no| X["Send to support"]
  V -->|yes| M["User with<br/>this email?"]
  M -->|yes| S["Store the new sub<br/>on that user"]
  M -->|no| C["Create the user"]
```

## 5. Cut over one application

```mermaid
flowchart LR
  B["Both providers<br/>registered"] --> F["Flip the app<br/>to Nauthera"] --> W["Watch"]
  W -->|healthy| K["Keep the old client<br/>until its tokens expire"]
  W -->|problems| RB["Flip back"]
```

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](https://www.nauthera.io/docs/v1/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](https://www.nauthera.io/docs/v1/adoption/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.
