loginos

How do I add SSO to an app that already has users?

loginos teamUpdated 7 min read

Short answer

Add SSO as a second way into the accounts you already have, not as a new user base. Let signed-in users connect the new login from their settings, so your app stores the provider’s user id against their account. Keep passwords working while people link, then decide whether to retire them. Never match accounts by an unverified email. With loginos, every token then carries your own user id.

Your app has people with their own accounts, usually an email address and a password. Now you want single sign-on (SSO): one identity provider that signs people in once and lets them into all of your apps. The protocol is the easy part. The hard part is making every SSO sign-in land in the right existing account, never in a duplicate and never in someone else’s.

You have three ways to connect the old accounts to the new login. Linking is the safest, and you can roll it out at your own pace, with passwords working the whole time.

What are your options for existing accounts?

Each way of bringing existing accounts to SSO answers one question: when someone signs in through the identity provider, which of your users is it? The provider names people by its own id. In OpenID Connect, the identity layer on top of OAuth 2.0, that id is the sub claim. Your app knows them by your user id. Something has to connect the two.

ApproachHow the accounts connectRiskWork
Link accountsA signed-in user connects the new login in settingsLow: the person proves both accountsA settings button and a callback
Import users into the providerYou copy accounts, and password hashes where possible, into the providerMedium: some people may have to reset their passwordExport, import, cutover
Match by email at first sign-inYour app picks the user with the same email addressHigh, unless the provider verified the emailLittle, which makes it tempting

Account linking lets people connect the new login to the account they already have, while they are signed in to your app. Your session proves who they are on your side. The provider’s sign-in proves who they are on the other side. Your server then stores the provider’s sub against your user id, and that SSO login opens that account from then on.

Nobody loses data and nobody signs up twice. The cost is time: only people who come back can link, so both logins run side by side for a while. The link flow, step by step, covers the data model, the security checks and conflicts.

Import users into the identity provider

Importing copies your user records into the provider, which then owns sign-in. People keep their password only if the provider can check your password hashes: the same algorithm, such as bcrypt, Argon2 or PBKDF2, with the same parameters. Everyone else resets their password at the first sign-in. Some providers can instead move people one by one, at their next login. The migration techniques compare both and show which providers accept which hashes.

After an import, tokens carry the provider’s id, not yours. You still need a mapping, unless the provider lets you store your own id on each imported user.

Match by email at first sign-in

Just-in-time (JIT) matching means your app takes the email address from someone’s first SSO sign-in and attaches the login to the user with that address. It is quick to build and risky. If the provider lets people sign up with any address without verifying it, an attacker can create an account with your user’s email and walk into their account.

The OpenID Connect spec says only iss and sub, together, identify a person reliably, and that email must not be used as a unique identifier (section 5.7). Libraries agree. Auth.js refuses to link by email unless you set an option named allowDangerousEmailAccountLinking. Supabase Auth links identities with the same email automatically, and its docs call linking to an unverified address insecure.

Only consider email matching when the provider says email_verified: true and you trust how it checks. For business customers, accept it only for a domain the customer has proved they own, with a DNS record.

How do you roll out SSO without locking anyone out?

A safe SSO rollout adds the new login next to passwords, lets people link at their own pace, and retires passwords last, if at all.

  1. Add the button. Put “Sign in with …” on your login page, next to the password form. When an SSO sign-in matches no linked account, don’t create a new one for someone who may already have one. Offer two choices: sign in the old way once to link, or create a new account.
  2. Link from settings. Add a Connect button to account settings, and offer it again right after a password sign-in, while the person is already in. Get the screens and wording right, because this is where people decide.
  3. Run both logins. Keep password sign-in and password reset working. Watch how many active users have linked, and remind those who haven’t.
  4. Decide about passwords. When most active users have linked, stop offering passwords to new accounts, then to everyone. Announce a date, keep a recovery path for the rest, and delete the password hashes once nothing reads them.

Keeping both logins is also a valid end state. SSO then becomes the faster way in, and passwords stay as a fallback.

What changes in your database?

Adding SSO to an existing app changes one thing in your database: a place for the provider’s user id. With one provider, a nullable, unique column on your users table is enough, like sso_sub. With several providers, use a separate table keyed by issuer and sub that points to your user id. Store the issuer with the sub, because a sub is only unique within one issuer.

Nothing else has to move. Your users table stays the source of truth, and your orders, permissions and foreign keys keep pointing to your own user ids.

With loginos, you don’t even need that column. You send your own user id when someone links, and every token carries it back as external_id.

How loginos does it

loginos is a hosted OpenID Connect provider for products that run on many domains, with account linking included. You keep your accounts. Each person connects a loginos account to theirs once, and every token after that names your user.

your-app.example/settings

J

Jane Doe

id u_1842

loginosLink loginos

loginos.io

Your app account

jane@example.com

Your loginos account

Jane Doe @jane

What your server reads from /oauth/token

{  "sub": "9b1f6c2e-…",  "preferred_username": "jane",  "roles": [    "member"  ]}
1/5Jane already has an account on your site. In its settings she clicks “Link loginos”.
  1. Create a platform in the developer console and add your domains.
  2. Add a “Link loginos” button to your account settings. Your server pushes the request to /oauth/par with your user id as external_id, so the browser never holds the id and can’t change it.
  3. The person signs in to loginos, or creates an account on the spot, and approves with both accounts on screen.
  4. loginos makes the link only when your server redeems the code. Every token then carries "external_id": "your-user-id".
  5. Add “Sign in with loginos” to your login page with the normal redirect flow, and sign in the user the token names:
After the code exchange
const { user } = await exchange(req, code, verifier); // POST /oauth/token, as in the quickstart
if (user.external_id) return signIn(await db.users.find(user.external_id));

// No link yet. Don't create a second account for someone who may already have one:
// ask them to sign in the old way once, then link from settings.
return NextResponse.redirect(new URL("/login?loginos=not-linked", req.url));

People who are new to your app can join through loginos: create their account at that first sign-in, then tell loginos the new id with PUT /api/v1/users/{sub}/external-id. The console’s Overview counts linked accounts, so you can follow the rollout. Link existing accounts has the complete code, and the Management API finds and unlinks members.

loginos doesn’t import password hashes, and it never links by email, because it doesn’t verify email addresses yet. It isn’t the right fit if you need SAML, connections to your customers’ identity providers, social logins like Google, or a sign-in page on your own domain.

Frequently asked questions

Do existing users have to create a new account for SSO?

No. With account linking, people keep the account they have and connect the new login to it once. Their data, settings and user id stay the same. Only a migration that can’t carry their password hash asks more of them, and then it is a password reset, not a new sign-up.

What happens if someone signs in with SSO before linking?

Your app gets a provider id it doesn’t know. Don’t guess by email. Ask whether they already have an account: if they do, they sign in the old way and link from settings, and if not, you create a new account.

Is SSO the same as social login?

Not quite. Social login, like “Sign in with Google”, reuses an account people already have at a large provider. SSO means one identity provider signs people in to several apps, so they sign in once. A social login provider can play that role, and so can a provider you run or rent. Both often run on OAuth 2.0 or OpenID Connect, so the linking steps are the same.

Do I need SAML to add SSO?

Only if your customers ask for it. Companies often want their employees to sign in through the company’s identity provider, and many of those support SAML 2.0 as well as OpenID Connect. For your own apps and domains, OpenID Connect is enough and simpler to build. loginos supports OpenID Connect only.

One account for every domain you run

loginos is an OpenID Connect provider with passkeys, a user pool shared by all of your domains, roles in every token, and account linking for the users you already have. Add your platform and its domains in a few minutes.

Keep reading

All answers