How do I add SSO to an app that already has users?
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.
| Approach | How the accounts connect | Risk | Work |
|---|---|---|---|
| Link accounts | A signed-in user connects the new login in settings | Low: the person proves both accounts | A settings button and a callback |
| Import users into the provider | You copy accounts, and password hashes where possible, into the provider | Medium: some people may have to reset their password | Export, import, cutover |
| Match by email at first sign-in | Your app picks the user with the same email address | High, unless the provider verified the email | Little, which makes it tempting |
Link accounts
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.
- 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.
- 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.
- Run both logins. Keep password sign-in and password reset working. Watch how many active users have linked, and remind those who haven’t.
- 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
Jane Doe
id u_1842
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" ]}- Create a platform in the developer console and add your domains.
- Add a “Link loginos” button to your account settings. Your server pushes the request to
/oauth/parwith your user id asexternal_id, so the browser never holds the id and can’t change it. - The person signs in to loginos, or creates an account on the spot, and approves with both accounts on screen.
- loginos makes the link only when your server redeems the code. Every token then carries
"external_id": "your-user-id". - Add “Sign in with loginos” to your login page with the normal redirect flow, and sign in the user the token names:
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
- Existing usersHow do I link existing user accounts to an OAuth login?Keep a table that maps each provider’s user id, the sub claim, to your own user id. Start the link from a page where the person is already signed in, send state and PKCE, and save the link on your server only after you exchange the code. Refuse a provider account that is already linked to someone else, and never link accounts by email.7 min read
- Existing usersHow do I migrate existing users to a new login system without making them sign up again?Move the accounts, not the people. Bulk-import users with their password hashes if the new system can verify your hash format, or migrate each person at their next login by checking the old password once. You can also skip the migration: keep your accounts and let signed-in users link the new login. loginos takes that last path, so people keep their account and connect loginos once.6 min read
- Existing usersHow can users connect their existing account to a new login provider?Give signed-in users a Connect button in their account settings. It sends them to the provider to sign in and approve, then back to your settings page with a clear confirmation. Your server makes the link only after it checks state and exchanges the code, and it never matches accounts by email. Then show which account is connected, with a way to disconnect it.7 min read
- Sign-in across domainsHow do I implement single sign-on across different domains?Use one OpenID Connect provider for all of your domains, and register each domain’s callback URL with it. On every domain, add a route that redirects to the provider with state and PKCE, and a callback that checks state, exchanges the code on your server and starts that domain’s own session. People signed in on one domain then pass through the others without a form.8 min read