Appearance
Sessions and tokens
Two kinds of session keep people signed in, and three kinds of token move between MicroAuth and your apps. This page explains how they fit together, then shows how to protect your own API with access tokens.
Sessions
On your sign in service. When someone signs in on your hosted pages, they get a session there, in a cookie only your sign in address can read. That session is what lets them sign in to your next app without typing anything. It ends after 30 days without use by default. Each visit to the hosted pages, and each time one of your apps refreshes tokens from that session, keeps it going.
In your app. After the code flow, your server starts its own session. With UserAuth, it's an encrypted cookie that holds the ID token and the refresh token:
__Host-microauthon https andmicroauthon plain http, HttpOnly,SameSite=Lax, encrypted with AES-256-GCM. A large session is split into.0,.1and so on.- POST, PUT, PATCH and DELETE requests that use the session must come from your own site, checked with
OriginorReferer. - It lasts as long as the sign in session, up to the SDK's
session_max_age(30 days by default). - To rotate the cookie secret, set
MICROAUTH_COOKIE_SECRET=new,old. The first secret encrypts, and all of them decrypt.
Lifetimes
You set these per service on the Sign in tab, in the Sessions and tokens card:
| Setting | Default | Range |
|---|---|---|
| Sign in session, counted from its last use | 30 days | 1 to 365 days |
| Access token, for your own apps | 10 minutes | 5 to 1440 minutes |
Access tokens for apps people connect last at most 10 minutes, even when the setting is longer.
Authorization codes last 60 seconds and work once. If a code is used a second time, the refresh token from its first use stops working at once, and its access token runs out on its own.
A new access token lifetime applies to tokens issued after the change. Tokens already issued keep the lifetime they were issued with.
Refresh tokens
A refresh token gets fresh tokens without bothering the person.
- It changes on every use. If two requests refresh at the same moment, a 30 second grace lets both succeed. Any later use of an old refresh token revokes the whole chain, because it means the token was copied.
- Your own apps get refresh tokens tied to the sign in session, so signing out ends them.
- Apps that ask for
offline_access, and apps people connect, get refresh tokens tied to their grant. They survive sign out, end when the person disconnects the app, and expire after 30 days unused or a year at most. - Turning refresh tokens off for one of your apps, or deleting the app, ends its refresh tokens at once. An app you block can't refresh until you unblock it.
Each refresh works out the person's roles and permissions again, which is how changes reach your app:
| What happens | When your app notices |
|---|---|
| You change someone's roles | At the next refresh, within one access token lifetime |
| You suspend someone | The next refresh fails, so the SDK signs them out |
| They sign out, or you sign them out everywhere | The next refresh fails |
| They disconnect an app, or you block it | That app's next refresh fails |
None of these touch access tokens already issued. They keep working until they expire, because your API checks them without asking MicroAuth. That is at most 10 minutes for apps people connect, and your access token lifetime, 10 minutes by default, for your own apps.
UserAuth refreshes 30 seconds before the access token expires, once per session even under parallel requests. If MicroAuth can't be reached, the session keeps working for up to 5 minutes past expiry, then routes get 503 with Retry-After: 5 until MicroAuth answers again.
Signing out
POST /auth/logout in your app clears the app's cookie, revokes the refresh token and sends the browser to your sign in service's logout endpoint with the ID token. Because the ID token and a registered sign out URL come with it, the person is signed out of the hosted pages right away and comes back to your app.
Asking someone to sign in again
Some pages deserve a fresh sign in, such as changing billing details. Pass max_age to UserAuth to make people sign in again when their last sign in is older than that many seconds. For one page only, send people to auth.login_url(next="/billing", max_age=600), which asks them to sign in again unless they did in the last 10 minutes. Your own OAuth client can send max_age or prompt=login on the authorization request. Someone who is still signed in to the hosted pages confirms it's them instead of signing in from scratch, and that counts as a fresh sign in. The ID token's auth_time says when they last signed in or confirmed, and amr says how:
| How they signed in | amr |
|---|---|
| Password | pwd |
| Password, then an authenticator or recovery code | pwd otp mfa |
| Password, then a passkey | pwd swk user mfa |
| Passkey | swk user mfa |
| Code sent by email, or an invite link | otp |
| Another provider, such as Google | fed |
| Another provider, then an authenticator or recovery code | fed otp mfa |
A passkey that can sync across devices shows as swk, and one bound to a single device as hwk. Confirming it's them later adds the values for the method they used, so a person who signed in with a provider and confirmed with a passkey has fed swk user mfa.
Protect an API with access tokens
When other apps, scripts or your own frontend call an API of yours, check MicroAuth access tokens with TokenAuth.
- Register the API under APIs and MCP servers with its URL, such as
https://api.example.com, and choose the permissions it exposes. - Set
MICROAUTH_ISSUERandMICROAUTH_RESOURCEwhere the API runs. - Require scopes per route:
python
from fastapi import Depends, FastAPI
from microauth_fastapi import TokenAuth, TokenClaims
app = FastAPI()
api = TokenAuth(app) # reads MICROAUTH_ISSUER and MICROAUTH_RESOURCE
@app.get("/posts")
async def posts(token: TokenClaims = Depends(api.require("posts:read"))):
return {"user": token.sub, "app": token.client_id}bash
MICROAUTH_ISSUER=https://acme.microauth.dev
MICROAUTH_RESOURCE=https://api.example.comWhat TokenAuth checks, with no call to MicroAuth on the request path:
- The token is an RS256 JWT of type
at+jwt, signed by your issuer, and unexpired. - Your API's URL is in
aud. A token for another server is refused. - The token holds every scope the route asks for.
A missing or bad token gets 401 with a WWW-Authenticate header that points to the API's metadata, which TokenAuth serves. It lives at /.well-known/oauth-protected-resource with the API's path added after it, such as /.well-known/oauth-protected-resource/v1 for https://api.example.com/v1, and at the bare well-known URL only for an API without a path. A token without a required scope gets 403 with error="insufficient_scope". Signing keys are cached and refreshed every 15 minutes, and right away when a token names a key the SDK hasn't seen.
To accept API keys your users create as well, add api_keys=True. To serve your own web app's browser sessions from the same routes, pass sessions=auth with your UserAuth.
Call your API from your web app
A web app with UserAuth can get an access token for one API at sign in and call it from the server:
python
import httpx
from fastapi import Depends, FastAPI, Request
from microauth_fastapi import User, UserAuth
app = FastAPI()
auth = UserAuth(app, resource="https://api.example.com", keep_access_token=True)
@app.get("/feed")
async def feed(request: Request, user: User = Depends(auth.login_required)):
token = await auth.access_token(request)
async with httpx.AsyncClient() as client:
r = await client.get("https://api.example.com/posts", headers={"Authorization": f"Bearer {token}"})
return r.json()Add the API's permissions to scopes, such as scopes=("openid", "profile", "email", "roles", "posts:read"). Keeping the access token makes the cookie about twice as large.
Other languages
Any JWT library can check access tokens: fetch the signing keys from /oauth/jwks and follow the access token rules. Without a JWT library, ask MicroAuth with introspection.