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.
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
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.Register a Confidential client
https:// callback route on your server as the redirect URI.Start the transaction on the server
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.Receive the callback on the server
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.Exchange the code on the server
/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.Keep tokens on the server
HttpOnly, Secure, and SameSite attributes.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.
Register a Public client
Open the system browser, never a WebView
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.Pick an app-bound redirect URI
Exchange the code on the device
/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.Store tokens in the platform credential store
Revoke on sign-out
/oauth/revoke when the user signs out, then clear the local credential store.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
stateand thecode_verifierper authorization request.stateis 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/authorizecannot be framed. Framing is same-origin only, so authorize in a popup or with a top-level redirect. For popups, your callback page can usewindow.openerto 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/revokeand clears your own session, and tell them to use it on shared devices. statestill applies. Popups and embeds don’t exempt you from generating and verifying it.
Anti-pattern: the token proxy
The shape looks reasonable and shows up often in generated native apps:- The app opens
/oauth/authorizein an embedded WebView. - The redirect URI points at the app’s own backend, whose callback page posts the
codeandstateinto the WebView through a JavaScript bridge. - The app posts that
codeplus its owncode_verifierto a backend endpoint such as/api/auth/token. - 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.
- 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.
statedoesn’t help.stateis a CSRF defense that binds a callback to a session. The attacker generated thestateand 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
Let the backend own the transaction
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.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_infrom 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_tokencall 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_granton refresh means re-authorize. An already-used or revoked refresh token fails withinvalid_grant. Do not retry. Send the user through/oauth/authorizeagain. Serialize refresh calls so concurrent requests do not invalidate each other’s tokens.- Revoke on sign-out. Call
/oauth/revokewith either token of the pair. See Revoking tokens for the details.
Where to keep tokens
Common questions
These come up in nearly every security review of an app built on the Mighty API.Token exchange requires a Client Secret — what architecture do you recommend for native apps?
Token exchange requires a Client Secret — what architecture do you recommend for native apps?
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.What's your guidance on claimed HTTPS callbacks and embedded-browser sign-in?
What's your guidance on claimed HTTPS callbacks and embedded-browser sign-in?
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.Do you support sender-constrained tokens, refresh-token rotation, and reuse detection?
Do you support sender-constrained tokens, refresh-token rotation, and reuse detection?
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
codeor acode_verifierfrom 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
S256is used on every flow, including confidential ones. stateis a fresh, cryptographically random, single-use value per request. It is bound to the session and verified on the callback, andissis 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
localStorageor 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_granttriggers re-authorization instead of a retry loop. - Sign-out calls
/oauth/revokeand clears local credentials. Embedded apps also expose an explicit disconnect. - Requested scopes are the narrowest set the feature needs, and the granted
scopein the token response is checked rather than assumed.