Sign in with sherlock
A complete, runnable Express app doing the OAuth 2.1 authorization-code flow end to end — login, callback, id_token, the Data API, and refresh.
Sign in with sherlock
Sign in with sherlock (redirect)
walks through each step of the flow on its own. This page is the
complete, runnable version — a small Express app with a /login,
/callback, and /refresh route, wired against the real
auth.shebang.pro endpoints. Register your own client first (either
path that concept page describes), set CLIENT_ID and a publishable key
for your database's Data API, and this runs as-is.
import express from "express";
import crypto from "node:crypto";
import { createClient } from "@supabase/supabase-js";
const ISSUER = "https://auth.shebang.pro/auth/v1";
const AUTHORIZE_ENDPOINT = `${ISSUER}/oauth/authorize`;
const TOKEN_ENDPOINT = `${ISSUER}/oauth/token`;
const CLIENT_ID = process.env.OAUTH_CLIENT_ID;
const REDIRECT_URI = "https://myapp.example.com/callback";
const DATA_API_URL = "https://api.shebang.pro/db/<your-slug>";
const DATA_API_PUBLISHABLE_KEY = process.env.SHERBASE_PUBLISHABLE_KEY;
const app = express();
const pendingLogins = new Map(); // state -> { verifier }
let session = null;
function b64url(buf) {
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function decodeJwtPayload(jwt) {
const [, payload] = jwt.split(".");
return JSON.parse(Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8"));
}
app.get("/login", (req, res) => {
const verifier = b64url(crypto.randomBytes(32));
const challenge = b64url(crypto.createHash("sha256").update(verifier).digest());
const state = b64url(crypto.randomBytes(16));
pendingLogins.set(state, { verifier });
const url = new URL(AUTHORIZE_ENDPOINT);
url.searchParams.set("response_type", "code");
url.searchParams.set("client_id", CLIENT_ID);
url.searchParams.set("redirect_uri", REDIRECT_URI);
url.searchParams.set("scope", "openid email profile");
url.searchParams.set("code_challenge", challenge);
url.searchParams.set("code_challenge_method", "S256");
url.searchParams.set("state", state);
res.redirect(url.toString());
});
app.get("/callback", async (req, res) => {
const { code, state, error } = req.query;
if (error) return res.status(400).send(`authorize error: ${error}`);
const entry = pendingLogins.get(state);
if (!entry) return res.status(400).send("unknown or expired state");
pendingLogins.delete(state);
const body = new URLSearchParams({
grant_type: "authorization_code",
code: String(code),
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: entry.verifier,
});
const tokenResp = await fetch(TOKEN_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
});
const tokenJson = await tokenResp.json();
if (!tokenResp.ok) return res.status(502).send("token exchange failed");
const claims = decodeJwtPayload(tokenJson.id_token);
session = {
access_token: tokenJson.access_token,
refresh_token: tokenJson.refresh_token,
expires_at: Date.now() + (tokenJson.expires_in || 3600) * 1000,
claims,
};
// Call the Data API as this signed-in user -- row-level security sees
// them as auth.uid(), same as any other supabase-js session.
const supabase = createClient(DATA_API_URL, DATA_API_PUBLISHABLE_KEY, {
global: { headers: { Authorization: `Bearer ${session.access_token}` } },
});
const { data: notes } = await supabase.from("notes").select("*").order("id");
res.send(`signed in as ${claims.email} (${notes?.length ?? 0} notes)`);
});
app.get("/refresh", async (req, res) => {
if (!session) return res.status(400).send("not signed in");
const body = new URLSearchParams({
grant_type: "refresh_token",
refresh_token: session.refresh_token,
client_id: CLIENT_ID,
});
const tokenResp = await fetch(TOKEN_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
});
const tokenJson = await tokenResp.json();
if (!tokenResp.ok) return res.status(502).send("refresh failed");
session.access_token = tokenJson.access_token;
session.refresh_token = tokenJson.refresh_token || session.refresh_token;
session.expires_at = Date.now() + (tokenJson.expires_in || 3600) * 1000;
res.send("refreshed");
});
app.listen(3000);
What to notice:
/loginbuilds the PKCE challenge andstate, then redirects to sherlock's own authorize endpoint — your app never renders a password form./callbackis the only route that talks toTOKEN_ENDPOINTdirectly; everything after the exchange (decodingid_token, calling the Data API) runs with the tokens already in hand./refreshis a plaingrant_type=refresh_tokencall — no PKCE needed the second time, since the refresh token itself is the proof.- This example keeps its session in memory for clarity. A real app needs to persist it somewhere it can survive a restart — cookies, a session store, whatever your app already uses for sessions — that part is on you; it isn't specific to sherlock.
Next
The Data API — what auth.uid() and
row-level security look like from the database side of this same token.