@drstrain/oauth2
v0.1.0
Published
OAuth2 client for DrStrain SSO
Maintainers
Readme
@drstrain/oauth2
OAuth2 client for DrStrain SSO. Works in Node.js (≥20) and modern browsers.
Install
npm install @drstrain/oauth2Quickstart
Confidential client (server-side)
import { OAuth2Client } from '@drstrain/oauth2';
const client = new OAuth2Client({
issuer: 'https://sso.drstra.in',
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
redirectUri: 'https://yourapp.com/oauth2/callback',
});
// 1. Send the user to the authorize URL
const url = client.authorizeUrl({
scope: ['profile:read', 'offline_access'],
state: 'any_state',
});
res.redirect(url);
// 2. In your callback handler, exchange the code for tokens
const tokens = await client.exchangeCode({ code: req.query.code });
// tokens: { access_token, token_type, expires_in, scope, refresh_token? }
// 3. Call scope-protected endpoints with the access token
const me = await client.getProfile(tokens.access_token);Public client (browser SPA, with PKCE)
import { OAuth2Client } from '@drstrain/oauth2';
const client = new OAuth2Client({
issuer: 'https://sso.drstra.in',
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'https://yourapp.com/oauth2/callback',
// no clientSecret — public clients use PKCE instead
});
// Before redirect: generate verifier + challenge, stash verifier
const { verifier, challenge } = await OAuth2Client.generatePkce();
sessionStorage.setItem('pkce_verifier', verifier);
window.location.href = client.authorizeUrl({
scope: ['profile:read'],
state: crypto.randomUUID(),
codeChallenge: challenge,
});
// In your callback page: exchange code + verifier
const tokens = await client.exchangeCode({
code: new URL(location.href).searchParams.get('code')!,
codeVerifier: sessionStorage.getItem('pkce_verifier')!,
});
sessionStorage.removeItem('pkce_verifier');API
new OAuth2Client(config)
{
issuer: string; // SSO base URL, e.g. 'https://sso.drstra.in'
clientId: string;
clientSecret?: string; // confidential clients only
redirectUri: string;
}client.authorizeUrl(opts) → string
Build the URL to redirect the user to.
{
scope: Scope[]; // ['profile:read', 'offline_access', ...]
state?: string;
codeChallenge?: string; // for PKCE
codeChallengeMethod?: 'S256'; // default 'S256'
}client.exchangeCode(opts) → Promise<Tokens>
{
code: string;
codeVerifier?: string; // required for PKCE flows
}client.refreshToken(refreshToken) → Promise<Tokens>
Returns a fresh access token + a rotated refresh token. Always store the new refresh token.
Scope-protected endpoints
Each call requires an access token whose scope grants access. They throw OAuthError (status 403) if the scope is missing.
| Method | Required scope | Returns |
| ----------------------------------------- | ------------------ | ----------------------------- |
| client.getProfile(token) | profile:read | { sub, email, name, picture } |
| client.updateProfile(token, patch) | profile:write | updated profile |
| client.getTeams(token) | team:read | { memberOf, leaderOf } |
| client.manageTeamMember(token, opts) | team:write | { ok: true } |
updateProfile accepts { name?: string, avatar?: Blob } (multipart form). manageTeamMember accepts { teamId: number, userId: string, action: 'add' | 'remove' }.
OAuth2Client.generatePkce() → Promise<{ verifier, challenge }>
Generate a random PKCE verifier and its SHA-256 base64url challenge.
OAuthError
Thrown on any non-2xx response.
class OAuthError extends Error {
readonly code: string | undefined; // e.g. 'invalid_grant', 'insufficient_scope'
readonly status: number; // HTTP status
readonly body: { error?: string; error_description?: string; [k: string]: unknown };
}Scopes
| Scope | Purpose |
| ---------------- | ------------------------------------------------------------------------- |
| profile:read | Read the user's identity (sub, email, name, picture). |
| profile:write | Update the user's name and avatar. |
| team:read | List teams the user belongs to or leads. |
| team:write | Add/remove members from teams the user leads. |
| offline_access | Issue a refresh token alongside the access token. |
Tests
Tests live at the repo root in /tests and run end-to-end against a real wrangler dev worker — no mocks. See the root README for instructions.
License
MIT
