loginos

How can users connect their existing account to a new login provider?

loginos teamUpdated 7 min read

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.

  1. 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.”
  2. 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.
  3. 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.

StateCopyAction
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.

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

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”.

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_verified is false, 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_id matches the signed-in user:
Callback
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

All answers