Quick Start
This guide walks you through integrating Seal into your application. By the end, your users will be able to sign up, log in, and log out — using Seal’s hosted authentication UI.
How it works
Section titled “How it works”Seal uses a standard OAuth 2.0 authorization code flow:
- Your app redirects the user to Seal’s hosted login page
- After authentication, Seal redirects back to your app with an authorization code
- Your backend exchanges the code for an access token and user profile
Prerequisites
Section titled “Prerequisites”- A Seal account — sign up at seal.dev
- An environment created in the Seal portal (you get one automatically on signup)
- Your environment’s Client ID and Client Secret (found in the portal under Settings)
1. Register a redirect URI
Section titled “1. Register a redirect URI”Before Seal can redirect users back to your app after login, you need to register a redirect URI.
In the Seal portal, go to Settings → Redirect URIs and add your callback URL:
http://localhost:3000/callback2. Redirect users to login
Section titled “2. Redirect users to login”When a user clicks “Sign in” in your app, redirect them to Seal’s authorization endpoint:
https://auth.seal.dev/auth/authorize? response_type=code& client_id=YOUR_CLIENT_ID& redirect_uri=http://localhost:3000/callback& state=RANDOM_STATE_VALUEfrom urllib.parse import urlencode
params = urlencode({ "response_type": "code", "client_id": "YOUR_CLIENT_ID", "redirect_uri": "http://localhost:3000/callback", "state": "RANDOM_STATE_VALUE",})authorize_url = f"https://auth.seal.dev/auth/authorize?{params}"# Redirect the user to authorize_urlconst params = new URLSearchParams({ response_type: "code", client_id: "YOUR_CLIENT_ID", redirect_uri: "http://localhost:3000/callback", state: "RANDOM_STATE_VALUE",});const authorizeUrl = `https://auth.seal.dev/auth/authorize?${params}`;// Redirect the user to authorizeUrl| Parameter | Description |
|---|---|
response_type | Always code |
client_id | Your environment’s Client ID |
redirect_uri | Must match a registered redirect URI exactly |
state | A random string to prevent CSRF attacks. Store it in the user’s session and verify it when the callback arrives. |
The user will see Seal’s hosted login page, branded with your environment’s settings. They can sign up or sign in using whichever authentication methods you’ve enabled (password, magic link, SSO, etc.).
3. Handle the callback
Section titled “3. Handle the callback”After the user authenticates, Seal redirects them back to your redirect_uri with an authorization code:
http://localhost:3000/callback?code=AUTHORIZATION_CODE&state=RANDOM_STATE_VALUEFirst, verify that the state parameter matches the one you stored. Then exchange the authorization code for tokens:
curl -X POST https://auth.seal.dev/auth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=AUTHORIZATION_CODE" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "redirect_uri=http://localhost:3000/callback"import httpx
response = httpx.post( "https://auth.seal.dev/auth/token", data={ "grant_type": "authorization_code", "code": "AUTHORIZATION_CODE", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "redirect_uri": "http://localhost:3000/callback", },)tokens = response.json()const response = await fetch("https://auth.seal.dev/auth/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", code: "AUTHORIZATION_CODE", client_id: "YOUR_CLIENT_ID", client_secret: "YOUR_CLIENT_SECRET", redirect_uri: "http://localhost:3000/callback", }),});const tokens = await response.json();The response includes an access token, refresh token, and the authenticated user’s profile:
{ "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii...", "token_type": "Bearer", "expires_in": 300, "refresh_token": "seal_rt_a1b2c3d4e5f6...", "user": { "id": "usr_1234567890", "email": "jane@acme.com", "first_name": "Jane", "last_name": "Smith" }, "organization": { "id": "org_acme_corp", "name": "Acme Corp" }}4. Manage user sessions
Section titled “4. Manage user sessions”After a successful token exchange, you need to create a session in your own application. Seal handles authentication — your app handles session management.
Use the access token and user profile from the token exchange response to establish a session. How you do this depends on your framework, but the general approach is:
- Store the Seal refresh token securely (e.g. in your database, tied to the user)
- Create your own application session (cookie, JWT, etc.) so the user stays logged in
- When your session needs refreshing, use the stored refresh token to get a new access token from Seal
The token exchange response from step 3 contains everything you need to create a session:
user.id— the authenticated user’s unique identifieruser.email— the user’s email addressaccess_token— a short-lived JWT (default: 5 minutes) for verifying the authenticationrefresh_token— a long-lived token you should store to refresh access tokens later
How you create a session depends on your backend framework. Store the refresh token server-side and set a session cookie for the browser.
from fastapi import FastAPI, Responsefrom fastapi.responses import RedirectResponseimport httpx
app = FastAPI()
@app.get("/callback")async def callback(code: str, state: str, response: Response): # Exchange the authorization code for tokens (step 3) token_response = httpx.post( "https://auth.seal.dev/auth/token", data={ "grant_type": "authorization_code", "code": code, "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "redirect_uri": "http://localhost:3000/callback", }, ) tokens = token_response.json()
user = tokens["user"] refresh_token = tokens["refresh_token"]
# TODO: Store the refresh token in your database tied to the user # TODO: Create or update the user in your database
# Set a session cookie for the browser redirect = RedirectResponse(url="/") redirect.set_cookie( key="session", value=tokens["access_token"], httponly=True, secure=True, samesite="lax", ) return redirectimport { Hono } from "hono";import { setCookie } from "hono/cookie";
const app = new Hono();
app.get("/callback", async (c) => { const code = c.req.query("code")!; const state = c.req.query("state")!;
// Exchange the authorization code for tokens (step 3) const tokenResponse = await fetch("https://auth.seal.dev/auth/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", code, client_id: "YOUR_CLIENT_ID", client_secret: "YOUR_CLIENT_SECRET", redirect_uri: "http://localhost:3000/callback", }), }); const tokens = await tokenResponse.json();
const { user, refresh_token } = tokens;
// TODO: Store the refresh token in your database tied to the user // TODO: Create or update the user in your database
// Set a session cookie for the browser setCookie(c, "session", tokens.access_token, { httpOnly: true, secure: true, sameSite: "Lax", }); return c.redirect("/");});Verify the access token
Section titled “Verify the access token”You can verify Seal’s access token signature using your environment’s public keys, available at:
GET https://auth.seal.dev/jwk/YOUR_CLIENT_IDThis returns a standard JWKS (JSON Web Key Set) that you can use with any JWT library to validate tokens. This is useful if you want to verify that the token exchange response is legitimate before creating a session.
# Fetch the JWKS for your environmentcurl https://auth.seal.dev/jwk/YOUR_CLIENT_IDUse the returned keys with your JWT library to verify the access_token signature and decode its claims.
import httpximport jwt # PyJWTfrom jwt import PyJWKClient
jwks_client = PyJWKClient( "https://auth.seal.dev/jwk/YOUR_CLIENT_ID", cache_jwk_set=True, # cache keys in memory lifespan=3600, # refresh cached keys every hour)
def verify_access_token(token: str) -> dict: signing_key = jwks_client.get_signing_key_from_jwt(token) payload = jwt.decode( token, signing_key.key, algorithms=["RS256"], ) return payloadimport { createRemoteJWKSet, jwtVerify } from "jose";
// JWKS is fetched once and cached automatically by joseconst jwks = createRemoteJWKSet( new URL("https://auth.seal.dev/jwk/YOUR_CLIENT_ID"));
async function verifyAccessToken(token: string) { const { payload } = await jwtVerify(token, jwks, { algorithms: ["RS256"], }); return payload;}Refresh an expired token
Section titled “Refresh an expired token”When you need a fresh access token (e.g. to re-verify the user’s identity), use the stored refresh token:
curl -X POST https://auth.seal.dev/auth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "refresh_token=seal_rt_a1b2c3d4e5f6..."response = httpx.post( "https://auth.seal.dev/auth/token", data={ "grant_type": "refresh_token", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "refresh_token": "seal_rt_a1b2c3d4e5f6...", },)tokens = response.json()const response = await fetch("https://auth.seal.dev/auth/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", client_id: "YOUR_CLIENT_ID", client_secret: "YOUR_CLIENT_SECRET", refresh_token: "seal_rt_a1b2c3d4e5f6...", }),});const tokens = await response.json();The response has the same shape as the initial token exchange — a new access token, the same refresh token, and the user’s profile.
Putting it together: auth middleware
Section titled “Putting it together: auth middleware”Here’s a complete middleware that verifies the access token on every request and automatically refreshes it when it expires:
import httpximport jwtfrom jwt import PyJWKClientfrom fastapi import FastAPI, Request, Responsefrom fastapi.responses import RedirectResponsefrom starlette.middleware.base import BaseHTTPMiddleware
CLIENT_ID = "YOUR_CLIENT_ID"CLIENT_SECRET = "YOUR_CLIENT_SECRET"
jwks_client = PyJWKClient( f"https://auth.seal.dev/jwk/{CLIENT_ID}", cache_jwk_set=True, lifespan=3600,)
class AuthMiddleware(BaseHTTPMiddleware): # Routes that don't require authentication PUBLIC_PATHS = {"/login", "/callback", "/health"}
async def dispatch(self, request: Request, call_next): if request.url.path in self.PUBLIC_PATHS: return await call_next(request)
token = request.cookies.get("session") if not token: return RedirectResponse(url="/login")
# Try to verify the token try: signing_key = jwks_client.get_signing_key_from_jwt(token) payload = jwt.decode( token, signing_key.key, algorithms=["RS256"] ) request.state.user = payload
except jwt.ExpiredSignatureError: # Token expired — try to refresh it new_token = await self.refresh_token(request) if not new_token: return RedirectResponse(url="/login")
# Verify the new token signing_key = jwks_client.get_signing_key_from_jwt(new_token) payload = jwt.decode( new_token, signing_key.key, algorithms=["RS256"] ) request.state.user = payload
# Continue with the request and set the new cookie response = await call_next(request) response.set_cookie( key="session", value=new_token, httponly=True, secure=True, samesite="lax", ) return response
except jwt.InvalidTokenError: return RedirectResponse(url="/login")
return await call_next(request)
async def refresh_token(self, request: Request) -> str | None: # Look up the stored refresh token for this user refresh_token = await get_refresh_token(request) # your DB lookup if not refresh_token: return None
async with httpx.AsyncClient() as client: response = await client.post( "https://auth.seal.dev/auth/token", data={ "grant_type": "refresh_token", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "refresh_token": refresh_token, }, )
if response.status_code != 200: return None
tokens = response.json() # Update the stored refresh token await save_refresh_token( # your DB update request, tokens["refresh_token"] ) return tokens["access_token"]
app = FastAPI()app.add_middleware(AuthMiddleware)Your route handlers can then access the authenticated user via request.state.user:
@app.get("/dashboard")async def dashboard(request: Request): user = request.state.user # decoded JWT payload return {"message": f"Hello {user['sub']}"}import { Hono } from "hono";import { getCookie, setCookie } from "hono/cookie";import { createRemoteJWKSet, jwtVerify, errors } from "jose";
const CLIENT_ID = "YOUR_CLIENT_ID";const CLIENT_SECRET = "YOUR_CLIENT_SECRET";
// JWKS is fetched once and cached automatically by joseconst jwks = createRemoteJWKSet( new URL(`https://auth.seal.dev/jwk/${CLIENT_ID}`));
const PUBLIC_PATHS = new Set(["/login", "/callback", "/health"]);
const app = new Hono();
// Auth middlewareapp.use("*", async (c, next) => { if (PUBLIC_PATHS.has(c.req.path)) { return next(); }
const token = getCookie(c, "session"); if (!token) { return c.redirect("/login"); }
try { const { payload } = await jwtVerify(token, jwks, { algorithms: ["RS256"], }); c.set("user", payload); } catch (err) { if (err instanceof errors.JWTExpired) { // Token expired — try to refresh it const newToken = await refreshToken(c); if (!newToken) { return c.redirect("/login"); }
const { payload } = await jwtVerify(newToken, jwks, { algorithms: ["RS256"], }); c.set("user", payload);
// Continue and set the new cookie on the response await next(); setCookie(c, "session", newToken, { httpOnly: true, secure: true, sameSite: "Lax", }); return; }
return c.redirect("/login"); }
return next();});
async function refreshToken(c: any): Promise<string | null> { // Look up the stored refresh token for this user const storedRefreshToken = await getRefreshToken(c); // your DB lookup if (!storedRefreshToken) return null;
const response = await fetch("https://auth.seal.dev/auth/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", client_id: CLIENT_ID, client_secret: CLIENT_SECRET, refresh_token: storedRefreshToken, }), });
if (!response.ok) return null;
const tokens = await response.json(); // Update the stored refresh token await saveRefreshToken(c, tokens.refresh_token); // your DB update return tokens.access_token;}
export default app;Your route handlers can then access the authenticated user via c.get("user"):
app.get("/dashboard", (c) => { const user = c.get("user"); // decoded JWT payload return c.json({ message: `Hello ${user.sub}` });});5. Log out the user
Section titled “5. Log out the user”To log a user out, redirect their browser to Seal’s logout endpoint. Seal will revoke the session, clear authentication cookies, and redirect the user to your post-logout destination.
GET https://api.seal.dev/auth/logout?sessionId=SESSION_ID&redirectTo=REDIRECT_URL| Parameter | Description |
|---|---|
sessionId | The Seal session ID to revoke. This is the sid claim from the access token JWT. |
redirectTo | Where to send the user after logout. Must match a registered logout redirect URI in your environment settings. |
Here’s how to add a logout button to your app:
import jwtfrom fastapi import Requestfrom fastapi.responses import RedirectResponsefrom urllib.parse import urlencode
@app.get("/logout")async def logout(request: Request): token = request.cookies.get("session") if not token: return RedirectResponse(url="/login")
# Decode the session ID from the access token (no verification needed here) claims = jwt.decode(token, options={"verify_signature": False}) session_id = claims.get("sid")
params = urlencode({ "sessionId": session_id, "redirectTo": "http://localhost:3000/login", }) logout_url = f"https://api.seal.dev/auth/logout?{params}"
# Clear your app's session cookie and redirect to Seal response = RedirectResponse(url=logout_url) response.delete_cookie(key="session") return responseimport { getCookie, deleteCookie } from "hono/cookie";import { decodeJwt } from "jose";
app.get("/logout", (c) => { const token = getCookie(c, "session"); if (!token) { return c.redirect("/login"); }
// Decode the session ID from the access token (no verification needed here) const claims = decodeJwt(token); const sessionId = claims.sid as string;
const params = new URLSearchParams({ sessionId, redirectTo: "http://localhost:3000/login", }); const logoutUrl = `https://api.seal.dev/auth/logout?${params}`;
// Clear your app's session cookie and redirect to Seal deleteCookie(c, "session"); return c.redirect(logoutUrl);});The logout flow works as follows:
- Your app extracts the
sid(session ID) from the access token - Your app clears its own session cookie and redirects the browser to Seal’s logout endpoint
- Seal revokes the session and clears authentication cookies on the Seal domain
- Seal redirects the user to your
redirectToURL
Next steps
Section titled “Next steps”You now have a working authentication flow. From here, you can:
- Magic Link authentication — enable passwordless sign-in with email verification codes
- Set up organizations — group users by customer with domain routing
- Add enterprise SSO — let your customers log in with their identity provider
- Explore the API reference — manage users, organizations, and sessions programmatically