Skip to content

OAuth & Authentication

This page covers every authentication mechanism the LenserFight MCP server supports. Implement this correctly once and your users will authorize with a single click — with no credentials stored in your product.


Token types

Token typeFormatLifespanWhen to use
MCP tokenlf_mcp_<64 hex>Long-lived (no expiry)Standard. Issued at end of OAuth flow. Use for all production requests.
Supabase JWTStandard JWT1 hour (configurable)Advanced. Use when you already have a short-lived Supabase session token.
Service role keySupabase JWT with service_role claimPermanent until rotatedstdio mode only. Never expose in production. Bypasses all RLS.

For virtually all provider integrations, you will use MCP tokens exclusively.


OAuth endpoints

All endpoints are served from the same base URL as the MCP server:

https://mcp.lenserfight.com
EndpointMethodPurpose
/.well-known/oauth-authorization-serverGETRFC 8414 discovery document
/.well-known/oauth-protected-resourceGETRFC 9728 protected resource metadata
/oauth/registerPOSTRFC 7591 dynamic client registration
/oauth/authorizeGETAuthorization endpoint — renders login form
/oauth/tokenPOSTToken endpoint — exchanges code for access token

Full OAuth 2.1 PKCE flow

Overview


bash
curl https://mcp.lenserfight.com/.well-known/oauth-authorization-server
json
{
  "issuer": "https://mcp.lenserfight.com",
  "authorization_endpoint": "https://mcp.lenserfight.com/oauth/authorize",
  "token_endpoint": "https://mcp.lenserfight.com/oauth/token",
  "registration_endpoint": "https://mcp.lenserfight.com/oauth/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"]
}

Parse and cache this. Use the endpoint values instead of hard-coding paths — they may change if the infrastructure moves.


Step 2 — Register your client

Call this once per product (or once per deployment). Save the client_id permanently.

http
POST https://mcp.lenserfight.com/oauth/register
Content-Type: application/json

{
  "client_name": "Acme AI Assistant",
  "redirect_uris": ["https://acme.example.com/api/mcp/callback"]
}

Response:

json
{
  "client_id": "lf_mcp_client_a1b2c3d4...",
  "client_name": "Acme AI Assistant",
  "redirect_uris": ["https://acme.example.com/api/mcp/callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code"],
  "response_types": ["code"]
}

No client secret is issued. The server uses PKCE exclusively. Do not send a client_secret field — it is ignored.

Multiple redirect URIs are allowed. Every authorization request must use one of the registered URIs exactly.


Step 3 — Generate PKCE parameters

typescript
import crypto from 'crypto'

// Generate code verifier (43–128 URL-safe chars)
const codeVerifier = crypto.randomBytes(32).toString('base64url')

// Derive code challenge (S256 = SHA-256 of verifier, base64url-encoded)
const codeChallenge = crypto
  .createHash('sha256')
  .update(codeVerifier)
  .digest('base64url')

Store codeVerifier in your server-side session. You will need it in Step 5.


Step 4 — Redirect the user to the authorization endpoint

Construct the URL:

https://mcp.lenserfight.com/oauth/authorize
  ?response_type=code
  &client_id=lf_mcp_client_a1b2c3d4...
  &redirect_uri=https://acme.example.com/api/mcp/callback
  &code_challenge=<base64url_sha256_of_verifier>
  &code_challenge_method=S256
  &state=<random_csrf_token>

The server renders a login form. The user enters their LenserFight credentials.

Important: Users must have a Lenser profile (a handle chosen at lenserfight.com) before they can authorize. If they have not completed onboarding, the login form will return an error.


Step 5 — Handle the callback

After successful login, the server redirects to:

https://acme.example.com/api/mcp/callback?code=lf_mcp_abc123...&state=<your_state>

Always verify state matches what you issued in Step 4 to prevent CSRF.


Step 6 — Exchange the code for an access token

http
POST https://mcp.lenserfight.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=lf_mcp_abc123...
&redirect_uri=https://acme.example.com/api/mcp/callback
&client_id=lf_mcp_client_a1b2c3d4...
&code_verifier=<the_verifier_from_step_3>

Response:

json
{
  "access_token": "lf_mcp_abc123...",
  "token_type": "bearer"
}

Design note: The code returned in the redirect callback is the lf_mcp_* bearer token. On POST /oauth/token, if the code already starts with lf_mcp_, the server returns it directly as the access token. This makes the flow compatible with clients (like Claude.ai) whose token exchange call comes from a cloud backend that cannot reach localhost — the token travels to the client through the browser redirect, not a server-to-server call.


Step 7 — Call the MCP server

Include the token on every request:

http
POST https://mcp.lenserfight.com/mcp
Authorization: Bearer lf_mcp_abc123...
Content-Type: application/json
mcp-session-id: <your_session_id>

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": { "name": "list_lenses", "arguments": { "limit": 5 } }
}

Request headers reference

HeaderValueRequired
AuthorizationBearer lf_mcp_<hex>Yes
Content-Typeapplication/jsonYes
mcp-session-idOpaque string you generate per conversation sessionRecommended

mcp-session-id is not required but improves performance for multi-turn sessions. The server creates a new in-memory session context for each request when it is absent.


Token lifecycle

MCP tokens are long-lived. They do not expire unless:

  • The user revokes the integration from the LenserFight settings.
  • A LenserFight admin deletes the row from lensers.mcp_tokens.

You do not need to implement token refresh. Store the token securely (e.g., encrypted in your database) and reuse it for all future calls from that user.


Using a Supabase JWT instead (advanced)

If you already have a short-lived Supabase JWT for the user (e.g., from a direct Supabase auth integration in your product), you can use it directly:

http
Authorization: Bearer <supabase_jwt>

The server calls sb.auth.getUser(token) to validate it, resolves the lenser_id via RPC, and creates a user-scoped client. Supabase JWTs expire after the session lifetime configured for the project (default 1 hour). For long-lived integrations, use MCP tokens instead.


Security considerations

  • Never log MCP tokens. They are long-lived and equivalent to credentials.
  • Verify state in the callback to prevent CSRF.
  • Never send a client_secret. The server is PKCE-only; no secret exists.
  • One token per user per product. If a user authorizes your product multiple times, each flow issues a new token. Old tokens remain valid until revoked.
  • The service role key bypasses all RLS. Only use it in stdio mode, never in production.

Re-authorization

If a token is revoked or returns 401, send the user back through the full OAuth flow (Steps 3–7). The client_id from Step 2 is reusable — you do not need to re-register.