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

# OAuth Client Architectures

> Choose an OAuth architecture that fits where your code runs and protects Mighty API tokens

## Overview

Start by answering these questions:

* **Where do the tokens live?** A Mighty API access token is a bearer credential that acts as the user. Anyone holding it can do everything that user approved.
* **Who can exchange an authorization code for tokens?** That party acts as your application.

The Client Secret alone does **not** make a flow safe. It proves that your application is calling the token endpoint, but not which browser session, device, or caller started the transaction. Bind each transaction to the component that started it with PKCE, one `code_verifier`, one `state` value, and one registered redirect URI. The same trusted party must hold these values from start to finish. Passing any of them across a trust boundary creates the [token proxy anti-pattern](#token-proxy), even if the secret is stored securely.

This page covers architecture. See [Authentication](/api/authentication) for request and response formats and [OAuth Applications](/oauth-applications) for application registration.

### Pick your architecture

| Where your code runs                                       | Register as              | Redirect URI style                                            | Tokens live                                              | Pattern                                                           |
| ---------------------------------------------------------- | ------------------------ | ------------------------------------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------- |
| Server-rendered web app                                    | Confidential             | `https://` route on your server                               | Server side, encrypted at rest                           | [Backend web apps](#backend-web-apps)                             |
| Browser or mobile front end **with** a backend you control | Confidential             | `https://` route on your server                               | Server side; the client gets your own session credential | [Backend web apps](#backend-web-apps)                             |
| Native iOS or Android app, no backend in the token path    | Public                   | Claimed HTTPS (Universal Link / App Link), then custom scheme | Keychain / Keystore on the device                        | [Native mobile and desktop apps](#native-mobile-and-desktop-apps) |
| Desktop app or CLI                                         | Public                   | Loopback: `http://127.0.0.1:PORT/callback`                    | OS credential store                                      | [Native mobile and desktop apps](#native-mobile-and-desktop-apps) |
| Single-page app with no backend at all                     | Public                   | `https://` page on your own origin                            | Memory only, never `localStorage`                        | [Single-page and embedded apps](#single-page-and-embedded-apps)   |
| Web app embedded in a Mighty Network                       | Confidential (preferred) | `https://` route on your server                               | Server side                                              | [Single-page and embedded apps](#single-page-and-embedded-apps)   |

<Note>
  Every Mighty API token acts as a signed-in user. The Mighty API rejects application-only tokens, so a user must always sign in and approve access.
</Note>

## Backend web apps

Use a **Confidential** client when your application has a server you control. This is the standard Authorization Code flow and the architecture that AI coding tools such as Lovable and Replit typically generate for an app with a backend.

<Steps>
  <Step title="Register a Confidential client">
    In **Network Admin** > **Integrations** > **OAuth Applications**, create an application with client type **Confidential**. Register the exact `https://` callback route on your server as the redirect URI.
  </Step>

  <Step title="Start the transaction on the server">
    On sign-in, your **backend** generates the `state` nonce and the PKCE `code_verifier`, stores both in the server-side session, and redirects the browser to `/oauth/authorize` with the `state` and the `S256` `code_challenge`.
  </Step>

  <Step title="Receive the callback on the server">
    Mighty redirects the browser to your registered callback route. Your backend compares the returned `state` against the value in that session and rejects anything that doesn't match. It also checks that the `iss` parameter matches the `issuer` in the Network's metadata document, per [RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207).
  </Step>

  <Step title="Exchange the code on the server">
    The backend calls `/oauth/token` with the code, the `code_verifier` from its own session, the Client ID, and the Client Secret. The verifier is never sent to the browser and never accepted from it.
  </Step>

  <Step title="Keep tokens on the server">
    Store the access and refresh tokens on the server, encrypted at rest and associated with your user record. The browser receives only your application's session cookie. Configure the cookie with `HttpOnly`, `Secure`, and `SameSite` attributes.
  </Step>
</Steps>

This pattern also works as a **backend for frontend (BFF)**. If your single-page app, mobile app, or Mighty embed has a backend, let the backend handle the entire OAuth transaction. The client authenticates to your backend with your own session credential, and the backend calls the Mighty API on the user's behalf. The client never receives a Mighty token.

<Warning>
  A backend for frontend must own the **whole** transaction: it generates `state` and the verifier, receives the callback, and exchanges the code. A backend that accepts a `code` and `code_verifier` from a client is an exploitable [token proxy](#token-proxy), not a backend for frontend.
</Warning>

## Native mobile and desktop apps

Register a **Public** client and run Authorization Code with PKCE entirely on the device. Do not use a Client Secret or put a backend in the token path. PKCE is enforced for public clients: `/oauth/authorize` rejects requests without `code_challenge`, and `/oauth/token` rejects exchanges without `code_verifier`.

```text theme={null}
Native app            System browser         Mighty
    |                       |                   |
    |  open authorize URL   |                   |
    |  (state + S256        |                   |
    |   challenge)          |                   |
    |---------------------->|  /oauth/authorize |
    |                       |------------------>|
    |                       |  sign in, consent |
    |                       |<------------------|
    |  redirect to the app  |                   |
    |  claimed https:// URI |                   |
    |<----------------------|                   |
    |                                           |
    |  POST /oauth/token                        |
    |  code + code_verifier + client_id         |
    |------------------------------------------>|
    |                                           |
    |  access_token + refresh_token             |
    |<------------------------------------------|
```

<Steps>
  <Step title="Register a Public client">
    Choose the **Public** client type. Mighty issues no secret because none is needed.
  </Step>

  <Step title="Open the system browser, never a WebView">
    Per [RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252), use `ASWebAuthenticationSession` on iOS, Custom Tabs on Android, and the default system browser on desktop. The operating system returns the callback to your app, so the authorization code never passes through a surface your app renders.

    Never load `/oauth/authorize` in an embedded WebView, and never bridge the code out of a web view with something like `window.ReactNativeWebView` or a JavaScript interface. An embedded browser puts your application between the user and Mighty's login page, which defeats the point of redirecting to Mighty in the first place.
  </Step>

  <Step title="Pick an app-bound redirect URI">
    Redirect URIs must match exactly, including the scheme, host, port, and path. Wildcards and fragments are rejected.

    | Preference       | Redirect URI                                                                                         | Why                                                                                                                            |
    | ---------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
    | **Best**         | Claimed HTTPS: `https://app.example.com/oauth/callback` via iOS Universal Links or Android App Links | Ownership is proven by a file you host on the domain, so another app can't claim it.                                           |
    | **Acceptable**   | Custom scheme: `com.example.app:/oauth/callback`                                                     | Accepted by Mighty, but the operating system does not guarantee uniqueness. Another app can register the same scheme.          |
    | **Desktop only** | Loopback: `http://127.0.0.1:PORT/callback`, `http://localhost:...`, `http://[::1]:...`               | Allowed by [RFC 8252 §7.3](https://datatracker.ietf.org/doc/html/rfc8252#section-7.3). Mighty permits `http` only on loopback. |

    A claimed HTTPS URI can point to a **page on your own web server**. If the app is installed, the operating system intercepts the redirect and sends the code to the app. Otherwise, the page can prompt the user to install the app. The page must never forward the code to a WebView or another caller.
  </Step>

  <Step title="Exchange the code on the device">
    The app calls `/oauth/token` directly with `code`, `code_verifier`, `client_id`, and `redirect_uri`, but no secret. Authorization codes are single-use and expire in about ten minutes.
  </Step>

  <Step title="Store tokens in the platform credential store">
    Use Keychain on iOS and macOS, Keystore-backed encrypted storage on Android, and the OS credential manager on Windows and Linux. Refresh tokens on the device. See [Token handling](#token-handling).
  </Step>

  <Step title="Revoke on sign-out">
    Call `/oauth/revoke` when the user signs out, then clear the local credential store.
  </Step>
</Steps>

<Note>
  You can still use a backend for your **own** app data. Your backend should authenticate the user with your own session credential, not act as a Mighty token exchange endpoint. If it needs to call the Mighty API on the user's behalf, let it own the [backend flow](#backend-web-apps) from start to finish instead of relaying an exchange the app started.
</Note>

## Single-page and embedded apps

If you have any backend, use the [backend for frontend](#backend-web-apps) pattern. It keeps Mighty tokens out of the browser entirely, which no browser-side storage strategy can match.

With no backend, register a **Public** client and run Authorization Code with PKCE in the browser:

* Redirect to a page on your own origin, registered exactly.
* Generate `state` and the `code_verifier` per authorization request. `state` is required on every request to `/oauth/authorize`.
* Hold the access token in **JavaScript memory only**. Do not put tokens in `localStorage`, `sessionStorage`, or a cookie your scripts can read. A cross-site scripting bug would let an attacker steal the token.
* Expect the user to re-authorize when they reload the tab. That is the trade-off for not having a backend.

### Apps embedded in a Mighty Network

Hosts can embed your web app inside their Mighty Network in an iframe. Embedded apps have these additional requirements:

* **`/oauth/authorize` cannot be framed.** Framing is same-origin only, so authorize in a **popup** or with a **top-level redirect**. For popups, your callback page can use `window.opener` to return the result to the embedded page.
* **Turn on the sign-in overlay.** In the embed dialog, check **This embed uses a Mighty OAuth application**. Signed-out visitors then see a "Sign in to view this content" overlay before the iframe becomes interactive. See [Can I use external embeds?](/for-hosts/content-and-messaging/can-i-use-external-embeds#authenticated-iframes).
* **Offer an explicit disconnect.** Signing out of Mighty does not sign the member out of your embedded app. Give members a visible disconnect action that calls `/oauth/revoke` and clears your own session, and tell them to use it on shared devices.
* **`state` still applies.** Popups and embeds don't exempt you from generating and verifying it.

For a walkthrough of building and embedding one of these, see [Vibe Coding on Mighty](/for-hosts/analytics-and-integrations/vibe-coding-on-mighty).

<span id="token-proxy" />

## Anti-pattern: the token proxy

<Warning>
  If your backend accepts an authorization `code` (and often a `code_verifier`) from a client, adds the Client Secret, and returns the tokens, **the Client Secret provides no security**. Any caller who can obtain a code can use your secret. Fix the architecture instead of trying to guard the endpoint.
</Warning>

The shape looks reasonable and shows up often in generated native apps:

1. The app opens `/oauth/authorize` in an embedded WebView.
2. The redirect URI points at the app's own backend, whose callback page posts the `code` and `state` into the WebView through a JavaScript bridge.
3. The app posts that `code` plus its own `code_verifier` to a backend endpoint such as `/api/auth/token`.
4. The backend adds the server-held Client Secret, exchanges the code with Mighty, and returns the user's access and refresh tokens to the app.

Here is the same architecture from an attacker's point of view:

```text theme={null}
Malicious app         Mighty           Your backend
     |                   |                   |
     |  WebView sign-in, |                   |
     |  attacker's state |                   |
     |  and verifier     |                   |
     |------------------>|                   |
     |  victim signs in  |                   |
     |  code via WebView |                   |
     |  bridge           |                   |
     |<------------------|                   |
     |                                       |
     |  POST /api/auth/token                 |
     |  code + attacker's code_verifier      |
     |-------------------------------------->|
     |                   | + client_secret   |
     |                   |<------------------|
     |                   |  tokens           |
     |                   |------------------>|
     |  victim's access + refresh tokens     |
     |<--------------------------------------|
```

Why the usual defenses don't help:

* **PKCE doesn't help.** PKCE proves that whoever redeems the code also started the authorization request. Because the attacker started the request with their own verifier, the proof succeeds for them.
* **`state` doesn't help.** `state` is a CSRF defense that binds a callback to a session. The attacker generated the `state` and validates it themselves.
* **The Client Secret doesn't help.** Your backend applies the secret on behalf of every caller. Any caller who can trigger the secret can use it as a public credential.
* **The WebView is the other half of the problem.** Rendering Mighty's login page inside your app means your app can read the credentials, the session, and the code. Even with a fixed token endpoint, a WebView sign-in is a phishing surface.

### Correct fixes

Pick whichever matches your product, and implement one of them completely:

<CardGroup cols={2}>
  <Card title="Drop the backend from the token path" icon="mobile-screen">
    Register a **Public** client, sign in through the system browser, redirect to an app-claimed URI, and exchange the code on the device with PKCE and no secret. See [Native mobile and desktop apps](#native-mobile-and-desktop-apps).
  </Card>

  <Card title="Let the backend own the transaction" icon="server">
    The backend generates `state` and the verifier, keeps them in a server-side session, receives the callback **directly** from Mighty, exchanges the code, and never accepts a code or verifier from a client. See [Backend web apps](#backend-web-apps).
  </Card>
</CardGroup>

<Note>
  Device attestation, such as App Attest on iOS or Play Integrity on Android, can harden this architecture but cannot fix it. Attestation makes it harder to call your endpoint from an unofficial build, but the endpoint will still exchange any code it receives. Fix the trust boundary first, then add attestation if needed.
</Note>

## Token handling

Mighty issues **bearer** tokens. There is no DPoP or mutual TLS, so tokens are not bound to the clients that obtained them. A token grants access to whoever holds it. Protect tokens in storage and transit.

* **Read `expires_in` from the token response** rather than assuming a lifetime, and treat access and refresh tokens as high-value credentials for as long as they are valid. Refresh tokens expire and are invalidated by the events listed under [Refreshing tokens](/api/authentication#refreshing-tokens).
* **Refresh tokens rotate.** Every `grant_type=refresh_token` call returns a new access token *and* a new refresh token, and revokes the one you presented. Persist the new refresh token atomically before you use the new access token.
* **`invalid_grant` on refresh means re-authorize.** An already-used or revoked refresh token fails with `invalid_grant`. Do not retry. Send the user through `/oauth/authorize` again. Serialize refresh calls so concurrent requests do not invalidate each other's tokens.
* **Revoke on sign-out.** Call `/oauth/revoke` with either token of the pair. See [Revoking tokens](/api/authentication#revoking-tokens) for the details.

```bash theme={null}
curl https://my-community.mn.co/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=ACCESS_OR_REFRESH_TOKEN" \
  -d "client_id=YOUR_CLIENT_ID"
```

Members can disconnect your application from **Connected Apps** in their account settings. A host can also delete the OAuth application, which revokes every token issued under it. You cannot rotate a Client Secret programmatically. Delete the application and create a replacement instead.

### Where to keep tokens

| Platform             | Store tokens in                                                        | Never                                                       |
| -------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------- |
| iOS and macOS        | Keychain, with device-only accessibility                               | `UserDefaults`, property list files, the app bundle         |
| Android              | Keystore-backed encrypted storage                                      | Plaintext shared preferences, external storage, logs        |
| Desktop and CLI      | OS credential store: Keychain, Windows Credential Manager, `libsecret` | Plain configuration files, shell history, environment dumps |
| Browser (no backend) | JavaScript memory only                                                 | `localStorage`, `sessionStorage`, readable cookies          |
| Server               | Encrypted at rest, key in a secret manager                             | Source control, committed `.env` files, application logs    |

Never log a token, put one in a URL query string, or send one to a third party. This includes analytics and error-reporting services that capture request bodies.

## Common questions

These come up in nearly every security review of an app built on the Mighty API.

<AccordionGroup>
  <Accordion title="Token exchange requires a Client Secret — what architecture do you recommend for native apps?">
    Token exchange does not require a secret. Register a **Public** client and use Authorization Code with PKCE. Public clients receive no secret and authenticate at the token endpoint with `code_verifier` alone. The metadata document at `/.well-known/oauth-authorization-server` lists `none` and `client_secret_post` under `token_endpoint_auth_methods_supported`. Putting a backend in a native app's token path creates the [token proxy](#token-proxy) problem.
  </Accordion>

  <Accordion title="Do you support a public/native client with Authorization Code + PKCE and no secret? What app-bound redirect is recommended?">
    Yes. PKCE with `S256` is supported and **enforced** for public clients: authorization is rejected without `code_challenge`, and the exchange is rejected without `code_verifier`. `code` is the only supported response type.

    For redirect URIs, prefer **claimed HTTPS** URIs (iOS Universal Links, Android App Links), then a **custom scheme** such as `com.example.app:/oauth/callback`. Use **loopback** (`http://127.0.0.1`, `http://localhost`, `http://[::1]`) for desktop apps and CLIs. All URIs are matched exactly. Wildcards and fragments are rejected, and `http` is permitted only on loopback.
  </Accordion>

  <Accordion title="What's your guidance on claimed HTTPS callbacks and embedded-browser sign-in?">
    Use the system browser: `ASWebAuthenticationSession` on iOS, Custom Tabs on Android, per [RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252). **Never sign in inside an embedded WebView, and never bridge an authorization code out of a web view into native code.** A claimed HTTPS callback is the right target because the operating system proves domain ownership before routing the redirect to your app. Web apps embedded inside a Mighty Network are the one place a browser frame is involved, and even there the authorization happens in a popup or a top-level redirect, never in the iframe.
  </Accordion>

  <Accordion title="Do you support sender-constrained tokens, refresh-token rotation, and reuse detection?">
    Refresh tokens **rotate**: every refresh issues a new access token and a new refresh token, and immediately revokes the one presented. Replaying a rotated refresh token fails with `invalid_grant`. There is no token-family revocation beyond that.

    Sender-constrained tokens are **not** supported. Mighty supports neither DPoP nor mutual TLS. Tokens are bearer credentials, so keep them off untrusted surfaces and out of intermediaries. You can revoke tokens immediately through `/oauth/revoke`, through **Connected Apps** in a member's account settings, or by deleting the OAuth application.
  </Accordion>
</AccordionGroup>

## Review checklist

Hand this to your reviewer, or paste it into your AI coding assistant as the acceptance criteria for the auth code it writes.

* The client type matches where the code runs. Use Confidential only where a server keeps the secret and Public everywhere else.
* No Client Secret is present in any mobile binary, desktop build, browser bundle, or repository.
* No endpoint anywhere accepts an authorization `code` or a `code_verifier` from a client and exchanges it. The party that generated the verifier is the party that redeems the code.
* Sign-in happens in the system browser (`ASWebAuthenticationSession`, Custom Tabs) or a top-level page, never an embedded WebView. No authorization code crosses a WebView bridge.
* PKCE with `S256` is used on every flow, including confidential ones.
* `state` is a fresh, cryptographically random, single-use value per request. It is bound to the session and verified on the callback, and `iss` is also checked to confirm it matches the Network's issuer.
* Redirect URIs are registered exactly and use claimed HTTPS, a custom scheme, or loopback. They contain no wildcards or open redirectors and do not point to third-party pages.
* Tokens are stored in the platform credential store, server-side encrypted storage, or memory. They are never stored in `localStorage` or written to logs.
* Token lifetime comes from `expires_in`. No lifetime is hardcoded.
* Rotated refresh tokens are persisted atomically, refresh calls are serialized, and `invalid_grant` triggers re-authorization instead of a retry loop.
* Sign-out calls `/oauth/revoke` and clears local credentials. Embedded apps also expose an explicit disconnect.
* Requested scopes are the narrowest set the feature needs, and the granted `scope` in the token response is checked rather than assumed.

## Next steps

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/api/authentication">
    OAuth flows, scopes, and token lifecycle in detail.
  </Card>

  <Card title="OAuth Applications" icon="shield-keyhole" href="/oauth-applications">
    Register, configure, and manage the applications that issue tokens.
  </Card>

  <Card title="External embeds" icon="window-maximize" href="/for-hosts/content-and-messaging/can-i-use-external-embeds">
    Embed your app inside a Mighty Network, with the OAuth sign-in overlay.
  </Card>

  <Card title="Mighty API" icon="diagram-project" href="/api">
    Use your access token against the GraphQL API.
  </Card>
</CardGroup>
