Skip to content

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.

Seal uses a standard OAuth 2.0 authorization code flow:

Click Login Authorization request Authorization url Redirect url Redirect to login Login with credentials Authorization code Exchange code for token Access token + user profile Logged in User Your Application Seal Auth
  1. Your app redirects the user to Seal’s hosted login page
  2. After authentication, Seal redirects back to your app with an authorization code
  3. Your backend exchanges the code for an access token and user profile

  • 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)

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/callback

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_VALUE
ParameterDescription
response_typeAlways code
client_idYour environment’s Client ID
redirect_uriMust match a registered redirect URI exactly
stateA 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.).


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_VALUE

First, verify that the state parameter matches the one you stored. Then exchange the authorization code for tokens:

Terminal window
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"

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"
}
}

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:

  1. Store the Seal refresh token securely (e.g. in your database, tied to the user)
  2. Create your own application session (cookie, JWT, etc.) so the user stays logged in
  3. 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 identifier
  • user.email — the user’s email address
  • access_token — a short-lived JWT (default: 5 minutes) for verifying the authentication
  • refresh_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.

You can verify Seal’s access token signature using your environment’s public keys, available at:

GET https://auth.seal.dev/jwk/YOUR_CLIENT_ID

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

Terminal window
# Fetch the JWKS for your environment
curl https://auth.seal.dev/jwk/YOUR_CLIENT_ID

Use the returned keys with your JWT library to verify the access_token signature and decode its claims.

When you need a fresh access token (e.g. to re-verify the user’s identity), use the stored refresh token:

Terminal window
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..."

The response has the same shape as the initial token exchange — a new access token, the same refresh token, and the user’s profile.

Here’s a complete middleware that verifies the access token on every request and automatically refreshes it when it expires:

import httpx
import jwt
from jwt import PyJWKClient
from fastapi import FastAPI, Request, Response
from fastapi.responses import RedirectResponse
from 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']}"}

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
ParameterDescription
sessionIdThe Seal session ID to revoke. This is the sid claim from the access token JWT.
redirectToWhere 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 jwt
from fastapi import Request
from fastapi.responses import RedirectResponse
from 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 response

The logout flow works as follows:

  1. Your app extracts the sid (session ID) from the access token
  2. Your app clears its own session cookie and redirects the browser to Seal’s logout endpoint
  3. Seal revokes the session and clears authentication cookies on the Seal domain
  4. Seal redirects the user to your redirectTo URL

You now have a working authentication flow. From here, you can: