# How 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.

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](https://loginos.io/answers/link-existing-accounts-oauth.md) 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.

| 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](https://www.rfc-editor.org/rfc/rfc9700), 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.

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:

```ts title="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](https://loginos.io/docs/link-accounts.md) has the full flow, and the [Management API](https://loginos.io/docs/api.md) 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](https://loginos.io/answers/add-sso-to-existing-app.md), helps here. If someone loses every method, your account recovery process takes over, as it does for a forgotten password.

---

Updated 2026-10-14. Web page: https://loginos.io/answers/connect-existing-account-new-provider
