@ohlom/login
v0.1.0
Published
Login with Ohlom — OAuth 2.0 Authorization Code + PKCE SDK for browser (public/SPA) and Node (confidential server) clients.
Maintainers
Readme
@ohlom/login
A tiny, zero-dependency TypeScript SDK for Login with Ohlom — OAuth 2.0 Authorization Code with PKCE (S256) and OpenID Connect. Works in the browser (public / SPA client) and in Node (confidential, server-side client). ESM + types.
npm install @ohlom/loginPublic vs confidential clients
Register your app at dev.ohlom.com and pick the client type:
| Client | Where it runs | Secret | How to construct |
| --- | --- | --- | --- |
| Public | Browser / SPA / mobile / desktop | none (PKCE only) | createClient({ ... }) without clientSecret |
| Confidential | Your server (Node) | yes | createClient({ ..., clientSecret }) |
PKCE protects both. A confidential client additionally sends its
client_secret on the token request (client_secret_post).
Never put a
clientSecretin browser code. Use a public client there.
The SDK targets these Ohlom endpoints (base URL https://api.ohlom.com):
GET /oauth/authorizePOST /oauth/token(application/x-www-form-urlencoded)GET /oauth/userinfo(Authorization: Bearer …)GET /.well-known/openid-configuration,GET /oauth/jwks
Scopes: openid, profile, phone, catalog:read, orders:read,
orders:write, inventory:write, messages:send, media:write,
posts:write.
Browser (SPA, public client) — the easy path
Use the @ohlom/login/browser helpers; they persist the PKCE verifier + CSRF
state in sessionStorage across the redirect and validate state on the way
back.
import { createClient } from "@ohlom/login";
import {
signInRedirect,
handleRedirectCallback,
hasAuthParams,
} from "@ohlom/login/browser";
const client = createClient({
clientId: "ohlom_yourpublicclientid",
redirectUri: "https://app.example.com/callback",
scopes: ["openid", "profile"],
// no clientSecret → public client
});
// On your login button:
async function login() {
await signInRedirect(client); // redirects to Ohlom's consent page
}
// On your /callback page:
async function onCallback() {
if (!hasAuthParams()) return;
const { tokens } = await handleRedirectCallback(client);
// tokens.access_token, tokens.id_token, tokens.refresh_token
const me = await client.userinfo(tokens.access_token);
console.log(me.sub, me.name);
// Store tokens where you like (in-memory recommended). Then strip the query:
history.replaceState({}, "", "/callback");
}Manual browser flow (no helpers)
const req = await client.buildAuthorizeUrl({ nonce: "..." });
sessionStorage.setItem("verifier", req.verifier);
sessionStorage.setItem("state", req.state);
location.assign(req.url);
// …on callback:
const p = new URLSearchParams(location.search);
if (p.get("state") !== sessionStorage.getItem("state")) throw new Error("CSRF");
const tokens = await client.exchangeCode({
code: p.get("code")!,
verifier: sessionStorage.getItem("verifier")!,
});Node / Express (server-side, confidential client)
import express from "express";
import session from "express-session";
import { createClient } from "@ohlom/login";
// Node 18+ has global fetch. For older Node, pass `fetch` in options.
const client = createClient({
clientId: process.env.OHLOM_CLIENT_ID!,
clientSecret: process.env.OHLOM_CLIENT_SECRET!, // confidential
redirectUri: "https://server.example.com/auth/callback",
scopes: ["openid", "profile", "phone"],
});
const app = express();
app.use(session({ secret: process.env.SESSION_SECRET!, resave: false, saveUninitialized: false }));
app.get("/auth/login", async (req, res) => {
const { url, verifier, state } = await client.buildAuthorizeUrl();
// Persist verifier + state server-side, bound to this user's session.
(req.session as any).pkce = { verifier, state };
res.redirect(url);
});
app.get("/auth/callback", async (req, res) => {
const { code, state } = req.query as { code?: string; state?: string };
const saved = (req.session as any).pkce;
if (!saved || !state || state !== saved.state) return res.status(400).send("state mismatch");
const tokens = await client.exchangeCode({ code: code!, verifier: saved.verifier });
const me = await client.userinfo(tokens.access_token);
// Establish your own session; keep Ohlom tokens server-side only.
(req.session as any).user = { sub: me.sub, name: me.name };
(req.session as any).pkce = undefined;
res.redirect("/");
});
// Later, refresh the access token:
// const fresh = await client.refresh(storedRefreshToken);API
createClient(opts: ClientOptions): OhlomClientClientOptions: clientId, redirectUri, scopes, clientSecret?,
baseUrl? (default https://api.ohlom.com), fetch?.
OhlomClient
buildAuthorizeUrl({ state?, nonce?, redirectUri?, scopes? }) → Promise<{ url, verifier, state, nonce? }>— build the authorize URL; persistverifier+stateuntil callback.exchangeCode({ code, verifier, redirectUri? }) → Promise<TokenResponse>— authorization_code grant (addsclient_secretonly if confidential).refresh(refreshToken) → Promise<TokenResponse>— refresh_token grant.userinfo(accessToken) → Promise<UserInfo>— scoped OIDC claims.logout() → false— Ohlom has no server-side revocation/end-session endpoint; refresh tokens are single-use and rotate, access tokens expire in ~1h. "Logging out" = discarding the tokens you hold (the browser helperclearStoredSession()clears the in-flight PKCE session).- Properties:
isPublic,clientId,redirectUri,scopes,baseUrl,discoveryUrl.
PKCE helpers
generateVerifier(bytes = 32) → stringchallengeFromVerifier(verifier) → Promise<string>(S256)randomString(bytes = 16) → string
@ohlom/login/browser
signInRedirect(client, opts?) → Promise<void>handleRedirectCallback(client, url?) → Promise<{ tokens, state, nonce? }>(validatesstate, exchanges, clears storage)hasAuthParams(url?) → booleanclearStoredSession() → void
Errors
Non-2xx responses throw OhlomAuthError with status and the OAuth error
code (e.g. invalid_grant, invalid_client). The SDK never logs tokens,
secrets, or the PKCE verifier.
Security notes
- PKCE S256 is always used; the authorize request sends
code_challengeand the token request sendscode_verifier. stateis generated and (with the browser helpers) validated to prevent CSRF.- Keep access/refresh tokens out of
localStoragewhere you can; prefer memory or a secure server-side session. - Confidential clients must keep
clientSecretserver-side only.
License
MIT
