> ## Documentation Index
> Fetch the complete documentation index at: https://docs.member.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Log in with an external provider (OAuth2 / OIDC)

> Let hub members sign in with Google, Facebook, or a company SSO — member.dev as the relying party.

member.dev can act as an OAuth2/OIDC **relying party**, letting your hub members authenticate using an identity they already have — Google, Facebook, or any OIDC/OAuth2-compatible company SSO. This is the inverse of the [Login with Hub](/guides/login-with-hub) guide (where member.dev is the Identity Provider).

After authentication succeeds, the member receives a standard contact JWT — the same token used for every other member API call.

***

## Provider support matrix

| Provider                                             | Sign-up                                                         | Login                                          | How                                                 |
| ---------------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------- |
| **Google**                                           | Yes (JIT)                                                       | Yes                                            | OIDC — `id_token` signed by Google's hardcoded JWKS |
| **Facebook**                                         | No                                                              | Yes (after connect)                            | OAuth2 — server-side Graph API call                 |
| **Generic OIDC** (company SSO / Okta / Auth0 / etc.) | No — or Yes with [Verified-Domain SSO](#verified-domain-sso-v2) | Yes (after connect; or JIT if domain verified) | OIDC — admin-supplied discovery + JWKS              |
| **Generic OAuth2**                                   | No                                                              | Yes (after connect)                            | OAuth2 — admin-supplied userinfo endpoint           |
| **Instagram**                                        | Not supported v1                                                | Not supported v1                               | —                                                   |

**Google is the only provider that can create a new member account without domain verification.** For generic OIDC, hub admins can unlock auto-creation by verifying ownership of the member's email domain — see [Verified-Domain SSO](#verified-domain-sso-v2) below. All other providers require the member to already have an account and to have explicitly connected the provider (see [Connect flow](#connect-flow)).

***

## How it works

```
Member                                  mio-backend                    External Provider
  |                                          |                                |
  |--GET /api/v1/external-login/{id}/start-->|                                |
  |                                          | gen PKCE + state + nonce       |
  |                                          | set CSRF cookie                |
  |<--------302 to provider authorize URL----|                                |
  |                                          |                                |
  |------ authenticates at provider -------->|                                |
  |                                          |                                |
  |<--------302 /api/v1/external-login/callback?code=&state=--------------<--|
  |                                          |                                |
  |                                          | verify CSRF cookie             |
  |                                          | exchange code (server-to-server pinned HTTPS)
  |                                          | verify id_token / fetch userinfo
  |                                          | resolve or create contact      |
  |                                          | mint bootstrap code (60s)      |
  |<--------302 return_to#code=<code>--------|                                |
  |                                          |                                |
  |--POST /api/v1/external-login/session {code}-->                           |
  |<--------{access_token, token_type, expires_in} + Set-Cookie: mio_contact_refresh
```

The one-time `code` in the fragment is exchanged server-side for the real token pair. The bearer token **never appears in a URL, history, or referrer header**.

***

## Prerequisites

1. The hub admin has registered an external-login provider config (see [Admin: configure a provider](#admin-configure-a-provider)).
2. For Google: any member can log in (first-time creates an account using the verified email).
3. For Facebook / Generic: the member has already connected their external account while logged in (see [Connect flow](#connect-flow)).

***

## Login flow (frontend implementation)

### Step 1 — Redirect the member to the provider

```bash theme={null}
GET /api/v1/external-login/{provider_config_id}/start?return_to=https://app.example.com/after-login
```

| Parameter            | Required   | Notes                                                                                                                            |
| -------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `provider_config_id` | Yes (path) | The `id` of the provider config from the admin API                                                                               |
| `return_to`          | No         | Where to redirect after success. Must be in the hub's allowed redirect origins. Defaults to the hub's configured post-login URL. |

The backend validates `return_to`, generates PKCE + state + nonce, stores a server-side flow binding (10-minute TTL), sets an HttpOnly CSRF cookie, and **redirects the browser** (302) to the external provider's authorization URL.

**This is a browser redirect, not a fetch.** Trigger it by setting `window.location.href` or using an `<a>` element.

### Step 2 — Provider authenticates the member

The provider redirects back to:

```
GET /api/v1/external-login/callback?code=CODE&state=STATE
```

The backend handles this entirely server-side:

* Validates the CSRF cookie against the stored binding
* Exchanges the code for an access token / id\_token via a pinned, SSRF-guarded server-to-server call
* Verifies the id\_token signature (OIDC) or fetches userinfo (OAuth2)
* Resolves or creates the member's contact (see [Account linking](#account-linking))
* Stores a single-use bootstrap code (60-second TTL) bound to the member's contact

On success, the member's browser is redirected to:

```
{return_to}#code=<bootstrap_code>
```

On any error, the backend returns a generic `400 JSON` response. No internal details are exposed.

### Step 3 — Exchange the bootstrap code for a token pair

Your frontend extracts `code` from `window.location.hash` and POSTs it:

```bash theme={null}
curl -X POST "https://api.member.dev/api/v1/external-login/session" \
  -H "Content-Type: application/json" \
  -d '{"code": "BOOTSTRAP_CODE_FROM_FRAGMENT"}'
```

**Response (200):**

```json theme={null}
{
  "access_token": "eyJ...",
  "token_type": "bearer",
  "expires_in": 3600
}
```

The `mio_contact_refresh` HttpOnly cookie is also set automatically. Use `access_token` as the `Authorization: Bearer` token for all subsequent member API calls.

**Errors (400):**

```json theme={null}
{
  "error": "invalid_grant",
  "error_description": "Code not found or expired."
}
```

The bootstrap code is single-use and expires in 60 seconds. Do not replay it.

***

## Connect flow

Facebook and generic providers (OIDC / OAuth2) do not auto-create accounts. A member must explicitly connect the provider to their existing contact while they are logged in.

### Initiate a connect

```bash theme={null}
curl -X POST "https://api.member.dev/api/v1/external-login/{provider_config_id}/connect?return_to=https%3A%2F%2Fapp.example.com%2Fsettings%2Faccounts" \
  -H "Authorization: Bearer MEMBER_ACCESS_TOKEN"
```

`return_to` is a **query parameter** (not a JSON body). The endpoint accepts no request body.

**Response (200):**

```json theme={null}
{
  "authorize_url": "https://www.facebook.com/v19.0/dialog/oauth?..."
}
```

Redirect the member's browser to `authorize_url`. The backend sets the CSRF cookie as part of this response.

After the member authenticates at the provider, the callback runs the same verification path as the login flow. On success, the provider identity is linked to the member's contact and the browser is redirected to `return_to` (no new token is issued — the member is already logged in).

**After a successful connect, the member can use the normal login flow (Step 1–3 above) for all future sessions with that provider.**

***

## Account linking

The backend follows a strict account-linking decision table. The outcome is determined entirely by verified, server-side assertions — the browser never supplies identity.

| # | Situation                                                         | Result                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| - | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0 | Connect mode (logged-in member attaches provider)                 | Link identity to current contact. If the identity is already linked to a **different** contact: rejected.                                                                                                                                                                                                                                                                                                                                                                                                                        |
| 1 | Login — identity `(namespace, sub)` link already exists           | Use the linked contact. `ContactAuthenticated` event emitted.                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| 2 | Login — Google, verified email, email matches an existing contact | Link identity to existing contact. Mark email verified if not already.                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| 3 | Login — Google, verified email, no matching contact               | Create a new contact (race-safe). `ContactRegistered` + `ContactAuthenticated` events emitted.                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 4 | Login — Facebook / generic (untrusted provider), no existing link | Rejected. Member must connect the provider first (row 0). **Exception for `generic_oidc` with a verified domain (v2):** if the member's email belongs to a domain the hub has verified, a new account is created automatically (same as row 3). If an account with that email already exists, still rejected — the member must connect the provider while signed in. See [Verified-Domain SSO](#verified-domain-sso-v2). `generic_oauth2` does not qualify even with a verified domain (no signed `id_token` → no domain trust). |
| 5 | Login — Google, but no email claim                                | Rejected. Email is required for account creation/linking.                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |

**Why Google only for rows 2/3:** Google's token endpoint and JWKS are hardcoded constants in the backend — they cannot be overridden by admin configuration. This makes Google's `email_verified` claim trustworthy. All other providers are "untrusted" because their identity endpoints are admin-supplied, and a malicious hub owner could otherwise forge email claims to take over arbitrary member accounts.

***

## Security notes

* **CSRF:** every flow is CSRF-protected by a persistent `HttpOnly` CSRF cookie (`__Host-extlogin_csrf` in production). The binding stores a hash of the cookie; the callback re-hashes and compares in constant time. Concurrent flows from the same browser share the cookie — starting provider A then B does not cancel A's flow.
* **PKCE S256** is always used. The `code_verifier` lives only on the server in the flow binding.
* **No bearer token in URLs.** The bootstrap code is a short-lived, single-use opaque value. Redeeming it requires an Origin header match against the code's bound return-origin.
* **Token scope:** the issued contact token covers only the hub that initiated the login. A hostile hub cannot trigger a login that produces a token covering the member's other hubs.
* **Enumeration:** all callback failures return one generic error response. The specific failure reason is written to the audit log only.
* **Rate limits:** `/start`, `/callback`, `/connect`, and `/session` are rate-limited by IP.

***

## Worked example: Google login

```
1. Hub admin registers a Google provider config:
   POST /api/v1/teams/TEAM_ID/external-login-providers
   → {provider_kind: "google", client_id: "...", client_secret: "..."}
   → Receives provider config with id: "cfg_abc123"
   → callback_url (read from response) is registered in Google Console

2. Hub adds "Log in with Google" button:
   href="/api/v1/external-login/cfg_abc123/start?return_to=https://app.myhub.com/dashboard"

3. Alice (new member) clicks the button.
   → Browser redirects to Google OAuth consent screen.
   → Alice approves. Google redirects to /api/v1/external-login/callback?code=...&state=...

4. Backend verifies the id_token (hardcoded Google JWKS, RS256, nonce, aud, iss).
   → email_verified: true + no existing contact → creates Alice's contact (row 3).
   → Issues contact token pair → stores bootstrap code → 302 to:
     https://app.myhub.com/dashboard#code=BOOTSTRAP

5. Frontend extracts BOOTSTRAP from location.hash, POSTs to /session.
   → Receives {access_token: "eyJ..."} + mio_contact_refresh cookie.
   → Alice is logged in. Member API calls use the access_token.

6. Next time Alice logs in via Google → row 1 (existing link), no account creation.
```

***

## Worked example: Facebook connect then login

```
1. Hub admin registers a Facebook provider config (display_name: "Facebook").

2. Bob (existing member, logged in) goes to Account Settings.
   Frontend: POST /api/v1/external-login/{cfg_id}/connect (Bearer token)
   → Receives {authorize_url: "https://www.facebook.com/v19.0/dialog/oauth?..."}

3. Frontend redirects Bob to authorize_url.
   → Bob approves Facebook login. Callback runs.
   → Backend fetches identity from Graph API (appsecret_proof).
   → Links (provider_config_id, facebook_sub) → Bob's contact (row 0).
   → 302 to return_to (no new token — Bob is already logged in).

4. Next time Bob visits the hub:
   GET /api/v1/external-login/{cfg_id}/start
   → Callback resolves via row 1 (existing link) → issues token pair.
```

***

## Verified-Domain SSO (v2)

By default, a `generic_oidc` provider can only log in members who have **already connected** it (row 0 → row 1 in the account-linking table). With Verified-Domain SSO, a hub admin can unlock **automatic account creation** for members whose email belongs to a company domain the hub has proven it controls.

### How it extends v1

|                              | Google (v1) | Generic OIDC — no domain | Generic OIDC — domain verified (v2) |
| ---------------------------- | ----------- | ------------------------ | ----------------------------------- |
| Auto-create new account      | Yes         | **No**                   | **Yes**                             |
| Login after connect          | —           | Yes                      | Yes                                 |
| Merges into existing account | Yes         | No                       | **No (connect-first rule)**         |

The connect-first rule is permanent: if a member already has a member.dev account with `alice@acme.com`, logging in through the OIDC provider will not silently adopt that account. Alice must sign in with her existing method and [connect the provider](#connect-flow) while logged in.

### Set up in three steps

1. **Register a domain** — `POST /api/v1/teams/{team_id}/verified-domains {hub_id, domain}`. Returns the DNS TXT record to publish.
2. **Publish the TXT record** — add `_member-dev-challenge.<domain>` = `member-dev-verify=<token>` to your DNS provider.
3. **Trigger verification** — `POST /api/v1/teams/{team_id}/verified-domains/{id}/verify`. The backend resolves the TXT; on success the domain moves to `verified`.

From that point on, any new `@<domain>` member who authenticates through a `generic_oidc` provider on this hub gets an account created automatically (subject to the hub's `registration_enabled` setting).

See the [Verified-Domain SSO guide](/guides/external-login-verified-domains) for the full walkthrough, DNS record format, lifecycle management, and security notes.

***

## Admin: configure a provider

See the [External Login Provider configuration guide](/guides/external-login-admin) for the full admin API reference, per-provider required fields, and where to obtain the `callback_url` to register in the provider console.
