How can users connect their existing account to a new login provider?
Short answer
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.
You have added a new way to sign in, and people with existing accounts need to connect it. For them, it should take one click and one approval. For an attacker, the same moment is a chance to attach their own login to someone else’s account. A good connect flow is short for the owner and a dead end for everyone else.
The implementation covers the table and the callback code. Here the focus is what the person sees, and what can go wrong.
What does the person see when they connect a new login?
Connecting a new login provider takes the person through three screens, and only the first and the last are yours.
- Your settings page. Under a heading like “Sign-in methods”, the provider, here called Example ID, has a Connect button. One line says why: “Sign in with Example ID instead of your password.”
- The provider’s pages. The person signs in there, or creates an account, and approves your app. The provider shows your app’s name and what you asked for, so ask only for the scopes you need.
- Your settings page again. A confirmation says what happened: “Example ID is connected. Next time, you can sign in with it.” The row now shows which account is connected and offers Disconnect.
Send an email to the account’s address as well: “A new sign-in method was added to your account.” If the owner didn’t make the change, that email is how they find out.
Which states and copy does the settings page need?
Each state on a sign-in methods page should say what is true now and what the person can do next. Name the provider and the account, and never blame the person.
| State | Copy | Action |
|---|---|---|
| Not connected | “Sign in with Example ID instead of a password.” | Connect |
| Connected | “Connected as jane@example.com since 14 October 2026.” | Disconnect |
| Cancelled | “Nothing changed. You can connect Example ID anytime.” | Connect |
| Already in use | “This Example ID account is connected to another account here. Sign in to that account to disconnect it, or use a different Example ID account.” | Connect |
| Expired | “That request expired. Start again from here.” | Connect |
| Provider down | “We couldn’t reach Example ID. Try again in a minute.” | Try again |
| Last sign-in method | “Add a password or another sign-in method before you disconnect Example ID.” | Add a password |
Confirm a disconnect with a short dialog that says what changes: “You’ll sign in with your password from now on.” Ask for a recent sign-in before connecting or disconnecting, because both change how someone gets into the account.
Which attacks does a connect flow need to stop?
Three attacks target the moment a login is connected, and each has a simple defense.
Login CSRF from a missing state
Cross-site request forgery (CSRF) makes a victim’s browser send a request the victim didn’t mean to send. In a sign-in flow, an attacker who slips their own authorization code into the victim’s callback signs the victim in to the attacker’s account. That is login CSRF. In a connect flow the result is worse: your app attaches the attacker’s provider account to the victim’s account, and the attacker can then sign in as the victim.
The defense is a random, single-use state value, kept in the session that started the flow and checked at the callback, plus PKCE. RFC 9700, the OAuth security best practice, requires clients to prevent CSRF. PKCE does that when the provider supports it, and state covers providers that don’t. Use both.
Account takeover through unverified emails
When an app links accounts because the email addresses match, the provider’s email check becomes part of your security. An attacker signs up at a provider that doesn’t verify addresses, using the victim’s email, then signs in to your app and lands in the victim’s account. The reverse works too: the attacker registers on your site with the victim’s address first, and keeps access after the victim later signs in through a provider. A 2022 study presented at USENIX Security found at least 35 of 75 popular services open to such “pre-hijacking” attacks.
The defense is to link only from a signed-in session, and to treat email as contact data, not as identity.
Link pages opened in someone else’s browser
An attacker starts a connect flow on their own account, then sends the provider’s approval page to a victim. If your app made the link the moment someone approved, the victim’s provider login would now open the attacker’s account. The victim would sign in there later, and the attacker could read anything they saved.
The defense is to make the link only on your server, after the code exchange, in the browser session that started the flow. The victim’s browser has neither the state nor the PKCE verifier, so the flow dies at your callback.
What happens when someone revokes access at the provider?
Most providers let people remove an app from their account settings. After that, the provider usually stops honoring the tokens it gave your app, so stored access and refresh tokens fail. Your link stays in your database, and the person’s next sign-in through the provider asks them to approve your app again.
Some providers tell you right away. Sign in with Apple, for example, sends a consent-revoked notification to a server endpoint you register. Others send nothing, and you notice at the next failed token refresh or sign-in.
When you learn about a revocation, delete the stored tokens and mark the connection in settings as needing attention, so the person knows what happened.
How loginos does it
With loginos, the Connect button starts a link request from your server: a push to /oauth/par with your user id as external_id. The person then sees your platform’s name and logo, the account you named in external_label above their loginos account, what linking changes, and what you will get. They approve and land back on your settings page.
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" ]}loginos answers the three attacks like this:
- CSRF. PKCE is required for linking, and your callback checks
state, as in every flow. - Unverified emails. loginos never links by email. It doesn’t verify email addresses yet, so
email_verifiedisfalse, and you shouldn’t match on email either. - Link pages in another browser. loginos makes the link only when your server redeems the code, and your callback checks that the token’s
external_idmatches the signed-in user:
const { user } = await exchange(req, code, verifier); // POST /oauth/token, as in the quickstart
const me = await currentUser(req);
if (!me || user.external_id !== me.id) return new Response("That loginos account is linked to someone else.", { status: 400 });The states in your settings page map to what loginos returns. A cancel comes back as error=access_denied, and nothing changes. A request older than 30 minutes, or one already used, shows “This link has expired” on loginos, so offer Connect again. If the person’s loginos account is already linked to another of your accounts, loginos stops there and offers to use another loginos account.
A revocation removes the link too. When someone removes your platform’s access from their loginos dashboard, their next sign-in with loginos has no external_id, and the Management API shows removed_access: true for them. Ask them to sign in the old way and link again. To show the current state in settings, look the person up with GET /api/v1/users?external_id=…. To disconnect from your side, call DELETE /api/v1/users/{sub}/external-id, and the person stays a member. The link guide has the full flow, and the Management API covers unlinking.
loginos is one login provider, not a hub for others: it has no “Sign in with Google” or other social logins. If the provider you want to connect is one of those, the flow above still applies, but loginos isn’t that provider.
Frequently asked questions
Should connecting a new login provider require the password again?
Ask for a recent sign-in rather than always asking for the password. If the person signed in a few minutes ago, let the connect flow go ahead. If the session is days old, ask them to confirm with their current method first, as you would before a password change.
Can a user connect two accounts from the same provider?
Only if your data model allows it. A common rule is one account per provider per user, and people disconnect the old one first. Whatever you choose, one provider account must never open two of your accounts.
Do I need to tell users when a sign-in method is added?
Yes. Email the account’s address whenever a sign-in method is added or removed, with the provider’s name and the time. If the owner didn’t make the change, that email is their signal to act, so link it to your security settings.
What if someone loses access to their provider account?
Keep another way in. Encourage people to keep a password or a second connected provider, and block disconnecting the last one. Keeping passwords until most people have linked, as in the SSO rollout plan, helps here. If someone loses every method, your account recovery process takes over, as it does for a forgotten password.
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 add SSO to an app that already has users?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.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