Skip to main content

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, even if the secret is stored securely. This page covers architecture. See Authentication for request and response formats and OAuth Applications for application registration.

Pick your architecture

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.

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

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

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

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

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

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.
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.
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, not a backend for frontend.

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

Register a Public client

Choose the Public client type. Mighty issues no secret because none is needed.
2

Open the system browser, never a WebView

Per RFC 8252, 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.
3

Pick an app-bound redirect URI

Redirect URIs must match exactly, including the scheme, host, port, and path. Wildcards and fragments are rejected.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.
4

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

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

Revoke on sign-out

Call /oauth/revoke when the user signs out, then clear the local credential store.
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 from start to finish instead of relaying an exchange the app started.

Single-page and embedded apps

If you have any backend, use the backend for frontend 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?.
  • 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.

Anti-pattern: the token proxy

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

Drop the backend from the token path

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.

Let the backend own the transaction

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

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.
  • 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 for the details.
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

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.
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 problem.
Use the system browser: ASWebAuthenticationSession on iOS, Custom Tabs on Android, per RFC 8252. 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.
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.

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

Authentication

OAuth flows, scopes, and token lifecycle in detail.

OAuth Applications

Register, configure, and manage the applications that issue tokens.

External embeds

Embed your app inside a Mighty Network, with the OAuth sign-in overlay.

Mighty API

Use your access token against the GraphQL API.