@genzzi/oauth
v1.6.1
Published
Official Genzzi OAuth 2.0 + OIDC client SDK for SPAs. Secure, framework-agnostic PKCE flow with React, Vue & Solid bindings.
Maintainers
Readme
@genzzi/oauth
Official Genzzi OAuth 2.0 + OIDC client SDK for SPAs. Secure, framework-agnostic PKCE flow with React, Vue & Solid bindings.
Features
- OAuth 2.0 + OIDC Compliant — Full Authorization Code flow with PKCE (S256)
- Framework Agnostic Core — Use vanilla JS/TS or bind to React, Vue, or Solid
- Automatic Token Management — Refresh tokens, expiry detection, and rotation safety
- CSRF Protection — State validation built into every flow
- Nonce Support — OIDC replay attack prevention
- Event Lifecycle — Subscribe to
flow_started,token_received,logout, and more - TypeScript First — Fully typed with strict compiler settings
- Zero Runtime Dependencies — Core SDK has zero deps; framework bindings are peer deps
- Vite Plugin — Dev-time stack trace injection for better error reporting
Installation
# npm
npm install @genzzi/oauth
# yarn
yarn add @genzzi/oauth
# pnpm
pnpm add @genzzi/oauthPeer Dependencies
Framework bindings require their respective peer dependencies. Install only what you need:
# React
npm install react react-dom
# Vue
npm install vue
# Solid
npm install solid-jsAll peer dependencies are optional. The core SDK works without any framework.
Quick Start
1. React (Provider + Hook)
import { GenzziAuthProvider, GenzziButton, useGenzziAuth } from "@genzzi/oauth/react";
function App() {
return (
<GenzziAuthProvider>
<RootLayout />
</GenzziAuthProvider>
);
}
function RootLayout() {
const { isAuthenticated, user, logout, isLoading } = useGenzziAuth();
if (isLoading) return <div>Loading…</div>;
return isAuthenticated ? (
<Dashboard user={user} onLogout={logout} />
) : (
<GenzziButton
oauthConfig={{ client_id: "my-app-123" }}
onSuccess={(tokens) => console.log("Logged in!", tokens)}
onOAuthError={(err) => console.error("Auth failed:", err)}
/>
);
}2. Vanilla / Framework-Agnostic
import {
generatePKCE,
fetchClientConfig,
startAuthorizationFlow,
handleCallback,
} from "@genzzi/oauth";
// Step 1: Start the flow
async function signIn() {
const { verifier, challenge } = await generatePKCE();
const clientResponse = await fetchClientConfig("my-app-123");
startAuthorizationFlow({
config: { client_id: "my-app-123" },
clientResponse,
verifier,
challenge,
});
}
// Step 2: Handle the callback (run on your redirect URI page)
async function onMount() {
const { tokens, userInfo } = await handleCallback();
console.log("Access token:", tokens.access_token);
console.log("User:", userInfo);
}3. Vue & Solid
Vue and Solid bindings follow the same patterns. See Framework Guides below.
Core API Reference
PKCE
| Function | Description |
|----------|-------------|
| generatePKCE() | Generates a cryptographically secure PKCE verifier + S256 challenge pair |
| validatePKCEVerifier(verifier) | Validates a verifier against RFC 7636 requirements |
| deriveChallenge(verifier) | Reproduces the S256 challenge from a verifier |
Flow Engine
| Function | Description |
|----------|-------------|
| discover(authServerUrl?) | Fetches the OIDC discovery document (cached for 1 hour) |
| fetchClientConfig(clientId) | Fetches public OAuth config for a registered client |
| startAuthorizationFlow(options) | Redirects the user to the Genzzi authorization server |
| handleCallback() | Exchanges the authorization code for tokens |
| storeTokens(tokens) | Persists tokens to localStorage |
| getStoredTokens() | Retrieves stored tokens (returns null if expired) |
| clearStoredTokens() | Removes all stored tokens and user info |
| refreshAccessToken(refreshToken, tokenEndpoint) | Refreshes an expired access token |
| fetchUserInfo(accessToken) | Fetches OIDC UserInfo |
| introspectToken(token) | Introspects a token (RFC 7662) |
| revokeToken(token, tokenTypeHint?) | Revokes a token (RFC 7009) |
Token & JWT Helpers
| Function | Description |
|----------|-------------|
| isTokenExpired() | Checks if the stored access token is expired (with 1-minute buffer) |
| parseJwt(token) | Safely parses a JWT payload |
| validateIdTokenNonce(idToken, expectedNonce) | Validates the nonce claim in an ID token |
| getCachedUserInfo() | Returns cached user info from localStorage |
Events
import { onGenzziEvent } from "@genzzi/oauth";
const unsubscribe = onGenzziEvent((event) => {
console.log(event.type, event.detail);
// "flow_started" | "redirecting" | "callback_received" | "state_validated"
// "token_exchanging" | "token_received" | "token_refreshed" | "userinfo_received"
// "error" | "session_expired" | "logout"
});
// Cleanup
unsubscribe();React API Reference
GenzziAuthProvider
Wraps your app and manages global auth state.
<GenzziAuthProvider
tokenEndpoint="https://api.genzzi.in/api/v1/oauth/token" // optional
onEvent={(event) => analytics.track(event.type)} // optional
>
{children}
</GenzziAuthProvider>useGenzziAuth()
const {
isAuthenticated, // boolean
isLoading, // boolean
accessToken, // string | null
user, // GenzziUserInfo | null
error, // GenzziOAuthError | null
refresh, // () => Promise<void>
logout, // () => Promise<void>
} = useGenzziAuth();useGenzziUser(accessToken?)
Fetches and caches OIDC UserInfo independently.
const { user, isLoading, error, refetch } = useGenzziUser();useGenzziEvent(listener)
React hook wrapper around onGenzziEvent.
useGenzziEvent((event) => {
if (event.type === "token_received") {
toast.success("Welcome back!");
}
});GenzziButton / Button
Pre-styled OAuth sign-in button with automatic PKCE and callback handling.
<GenzziButton
oauthConfig={{ client_id: "my-app" }}
variant="primary" // "primary" | "secondary" | "ghost" | "danger" | "light" | "dark" | "link"
size="md" // "sm" | "md" | "lg" | "xl" | "2xl"
display="logo-text" // "logo-only" | "text-only" | "logo-text"
useTailwind={false} // Use Tailwind classes instead of inline styles
loading={false}
disabled={false}
onSuccess={(tokens) => {}}
onOAuthError={(err) => {}}
onEvent={(event) => {}}
>
Sign in with Genzzi
</GenzziButton>Configuration
GenzziOAuthConfig
interface GenzziOAuthConfig {
client_id: string; // Required — your app ID
authServerUrl?: string; // Override default server
scope?: GenzziScope[]; // Default: ["openid", "profile", "email"]
response_type?: "code" | "token" | "id_token";
code_challenge_method?: "S256" | "plain"; // Default: "S256"
token_endpoint_auth_method?: "none" | "client_secret_post" | "client_secret_basic";
client_secret?: string; // For confidential clients only
redirect_uri?: string; // Must match registered URI
backend_uri?: string; // Your token exchange proxy
nonce?: string; // Auto-generated if omitted
state?: string; // Auto-generated if omitted
prompt?: "none" | "login" | "consent" | "select_account";
max_age?: number; // Seconds since last auth
login_hint?: string; // Pre-fill email/username
acr_values?: string; // Step-up auth context
extraParams?: Record<string, string>; // Custom authorization params
}Error Handling
All errors are instances of GenzziOAuthError with structured metadata:
try {
await handleCallback();
} catch (err) {
if (err instanceof GenzziOAuthError) {
console.log(err.code); // "SECURITY_CSRF_DETECTED"
console.log(err.message); // Human-readable message
console.log(err.recoverable); // Can the user retry?
console.log(err.statusCode); // HTTP status (if from API)
console.log(err.context); // Additional debug info
}
}Install Global Error Reporter
import { installGenzziErrorReporter } from "@genzzi/oauth";
installGenzziErrorReporter(); // Catches uncaught GenzziOAuthErrorsVite Plugin
Enhances GenzziButton components with stack traces for better dev error reporting.
// vite.config.ts
import { genzziDevPlugin } from "@genzzi/oauth";
export default {
plugins: [genzziDevPlugin(), react()],
};Framework Guides
React
See Quick Start — React above.
Vue
import { createGenzziAuth } from "@genzzi/oauth/vue";
const auth = createGenzziAuth({ client_id: "my-app" });
app.use(auth);
// In components
const { isAuthenticated, user, logout } = useGenzziAuth();Solid
import { GenzziAuthProvider, useGenzziAuth } from "@genzzi/oauth/solid";
function App() {
return (
<GenzziAuthProvider>
<RootLayout />
</GenzziAuthProvider>
);
}TypeScript
The SDK is written in TypeScript with strict settings. All types are exported:
import type {
GenzziOAuthConfig,
GenzziTokenResponse,
GenzziUserInfo,
GenzziOAuthError,
GenzziEvent,
GenzziEventType,
GenzziButtonProps,
// ...and more
} from "@genzzi/oauth";tsconfig.json Recommendation
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true
}
}Security
- PKCE S256 is enforced by default for all public clients.
- State validation prevents CSRF attacks on every callback.
- Nonce validation prevents replay attacks in OIDC flows.
- Refresh token rotation detection automatically clears sessions on reuse.
- No client secrets are required for SPAs (public client flow).
Browser Support
Requires a modern browser with:
crypto.getRandomValues&crypto.subtle.digest(PKCE)fetchAPIlocalStorage/sessionStorage
HTTPS or
localhostis required for the Web Crypto API.
Scripts
| Command | Description |
|---------|-------------|
| npm run build | Build all targets with tsup |
| npm run dev | Watch mode development build |
| npm run typecheck | Run tsc --noEmit |
| npm run lint | ESLint on src/**/*.ts,tsx |
| npm test | Run Vitest in watch mode |
| npm run test:ci | Run Vitest once (CI) |
Contributing
Contributions are welcome! Please open an issue or pull request on GitHub.
- Fork the repository
- Create your feature branch (
git checkout -b feat/amazing-feature) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feat/amazing-feature) - Open a Pull Request
