Universal OAuth 2.0 + PKCE Client for Modern Applications
A comprehensive, production-ready OAuth 2.0 + PKCE client library that works seamlessly across Browser, Node.js, React Native, and Web Workers. Built with security, developer experience, and universal compatibility in mind.
Unlike most OAuth libraries that lock you into a specific environment, Zenuxs OAuth works everywhere:
- β Browser (Chrome, Firefox, Safari, Edge)
- β Node.js (Server-side authentication)
- β React Native (iOS & Android)
- β Web Workers (Background authentication)
- π PKCE (RFC 7636) - Protection against authorization code interception
- π‘οΈ CSRF Protection - Built-in state parameter validation
- π Secure Token Storage - Flexible storage options (Memory, Session, Local)
- β‘ Automatic Token Refresh - Seamless token renewal before expiration
- π« Token Revocation - Properly invalidate tokens on logout
- π¦ Zero Dependencies - Lightweight and fast
- π― TypeScript Support - Full type definitions included
- π Multiple Auth Flows - Redirect, Popup, and Manual flows
- π‘ Event System - React to authentication state changes
- π¨ Framework Agnostic - Works with React, Vue, Angular, Svelte, or vanilla JS
- π Comprehensive Documentation - Clear examples and API reference
<script src="https://unpkg.com/zenuxs-oauth@2.3.1/dist/zenux-oauth.min.js"></script>npm install zenuxs-oauth
# or
yarn add zenuxs-oauthimport ZenuxOAuth from 'zenuxs-oauth';const ZenuxOAuth = require('zenuxs-oauth');const oauth = new ZenuxOAuth({
clientId: "your-client-id",
authServer: "https://api.auth.zenuxs.in",
// If your authorization/consent UI is hosted separately (e.g. https://zenuxs.in)
// you can override it here. Otherwise it defaults to the same value as authServer.
authorizeServer: "https://accounts.zenuxs.in",
redirectUri: window.location.origin + "/callback.html",
scopes: "openid profile email",
storage: "sessionStorage"
});
// Login with popup
async function login() {
try {
const tokens = await oauth.login({ popup: true });
console.log("Logged in!", tokens);
} catch (error) {
console.error("Login failed:", error);
}
}
// Get user info
async function getUserInfo() {
const user = await oauth.getUserInfo();
console.log("User:", user);
}
// Logout
async function logout() {
await oauth.logout({ revokeTokens: true });
}
// Optional: Fetch provider metadata (OIDC discovery)
async function getProviderMetadata() {
const metadata = await oauth.getDiscoveryDocument();
console.log('OIDC metadata:', metadata);
}
// Optional: Fetch JWKS for token validation
async function getJwkSet() {
const jwks = await oauth.getJwks();
console.log('JWKS:', jwks);
}
// Optional: Fetch client info (public client metadata)
async function getClientInfo() {
const clientInfo = await oauth.getClientInfo('your-client-id');
console.log('Client info:', clientInfo);
}const ZenuxOAuth = require('zenuxs-oauth');
const oauth = new ZenuxOAuth({
clientId: process.env.CLIENT_ID,
authServer: "https://api.auth.zenuxs.in",
redirectUri: "https://yourapp.com/callback",
scopes: "openid profile email",
storage: "memory",
fetchFunction: require('node-fetch')
});
// Express.js route
app.get('/auth/login', async (req, res) => {
const authData = await oauth.login();
req.session.state = authData.state;
req.session.codeVerifier = authData.codeVerifier;
res.redirect(authData.url);
});
app.get('/auth/callback', async (req, res) => {
const tokens = await oauth.handleCallback(req.url);
req.session.tokens = tokens;
res.redirect('/dashboard');
});import ZenuxOAuth from 'zenuxs-oauth';
import { Linking } from 'react-native';
const oauth = new ZenuxOAuth({
clientId: "your-client-id",
authServer: "https://api.auth.zenuxs.in",
redirectUri: "myapp://callback",
scopes: "openid profile email",
storage: "memory"
});
async function login() {
const authData = await oauth.login();
await Linking.openURL(authData.url);
// Listen for callback
Linking.addEventListener('url', async (event) => {
if (event.url.startsWith('myapp://callback')) {
const tokens = await oauth.handleCallback(event.url);
console.log("Tokens:", tokens);
}
});
}// Redirects the entire page
oauth.login();// Opens authentication in a popup window
const tokens = await oauth.login({
popup: true,
popupWidth: 600,
popupHeight: 700
});// Get authorization URL for manual handling
const authData = await oauth.login();
console.log("Redirect user to:", authData.url);
// Handle callback manually with authData.state and authData.codeVerifierconst oauth = new ZenuxOAuth({
clientId: "your-client-id",
authServer: "https://api.auth.zenuxs.in",
autoRefresh: true, // Enable auto-refresh
refreshThreshold: 300 // Refresh 5 minutes before expiry
});
// Listen to refresh events
oauth.on('tokenRefresh', (newTokens) => {
console.log("Tokens automatically refreshed!");
});Zenuxs exposes a standard set of OpenID Connect + social scopes. The SDK will request them exactly as passed via scopes.
openid(required for ID tokens)profile(returns:name,preferred_username,given_name,family_name,picture,updated_at)email(returns:email,email_verified)discord/discord:profile(returns:discord.id,discord.username,discord.discriminator,discord.avatar,discord.email)discord:guilds(returns:discord_guilds)discord:join_server(returns:discord_join_serverboolean)github/github:profile(returns:github.id,github.username,github.name,github.avatar,github.email,github.bio,github.public_repos)github:repos(returns:github_repos)github:commit(returns:github_commitboolean)
console.log(ZenuxOAuth.supportedScopes);β Tip: Use
scopes: 'openid profile email'for most standard OpenID Connect flows.
Zenuxs OAuth emits a few useful lifecycle events. Use on() to subscribe:
// When the login URL is generated (before redirect/popup)
oauth.on('loginRequest', (authData) => {
console.log('Login started (redirect/popup URL):', authData.url);
});
// When login completes successfully
oauth.on('login', (tokens) => {
console.log('Logged in! Tokens:', tokens);
});
// When tokens are refreshed
oauth.on('tokenRefresh', (newTokens) => {
console.log('Tokens refreshed', newTokens);
});
// When token is found expired and refresh is about to happen
oauth.on('tokenExpired', () => {
console.log('Token is expired; refreshing...');
});
// When user logs out
oauth.on('logout', () => {
console.log('Logged out');
});
// Global error handler
oauth.on('error', (error) => {
console.error('OAuth error:', error);
});
// State value has changed (used for CSRF protection)
oauth.on('stateChange', (change) => {
console.log('State changed:', change);
});// Session Storage (default) - survives page reload, cleared on tab close
storage: "sessionStorage"
// Local Storage - persists across browser sessions
storage: "localStorage"
// Memory Storage - cleared on page reload (best for Node.js/React Native)
storage: "memory"
// Custom prefix for storage keys
storagePrefix: "myapp_auth_"// Check authentication status
if (oauth.isAuthenticated()) {
console.log("User is authenticated");
}
// Get current tokens
const tokens = oauth.getTokens();
// Check if token is expired
if (oauth.isTokenExpired()) {
await oauth.refreshTokens();
}
// Manually refresh tokens
const newTokens = await oauth.refreshTokens();
// Revoke specific token
await oauth.revokeToken(tokens.access_token, 'access_token');
// Revoke all tokens on logout
await oauth.logout({ revokeTokens: true });// Get user profile from userinfo endpoint
const user = await oauth.getUserInfo();
console.log(user.name, user.email, user.picture);
// Multiple userinfo endpoints supported
const oauth = new ZenuxOAuth({
clientId: "your-client-id",
authServer: "https://api.auth.zenuxs.in",
userinfoEndpoint: "/oauth/userinfo" // or custom endpoint
});// Get pre-configured fetch with automatic token injection
const authFetch = oauth.getAuthenticatedFetch();
// Use it like regular fetch
const response = await authFetch('https://api.yourapp.com/protected', {
method: 'GET'
});
// Automatically adds Authorization header and handles token refreshconst oauth = new ZenuxOAuth({
// Required
clientId: "your-client-id",
// Server Configuration
authServer: "https://api.auth.zenuxs.in",
authorizeEndpoint: "/oauth/authorize",
tokenEndpoint: "/oauth/token",
userinfoEndpoint: "/oauth/userinfo",
revokeEndpoint: "/oauth/revoke",
introspectEndpoint: "/oauth/introspect",
// OAuth Parameters
redirectUri: window.location.origin + "/callback.html",
scopes: "openid profile email offline_access",
responseType: "code",
// Security
usePKCE: true, // Enable PKCE
useCSRF: true, // Enable CSRF protection (browser only)
validateState: true, // Validate state parameter
// Storage
storage: "sessionStorage", // sessionStorage | localStorage | memory
storagePrefix: "zenux_oauth_",
// Token Management
autoRefresh: true, // Enable automatic token refresh
refreshThreshold: 300, // Refresh 5 minutes before expiry
// UI Configuration (Browser only)
popupWidth: 600,
popupHeight: 700,
popupFeatures: "toolbar=no,location=no,status=no,menubar=no",
// Lifecycle Callbacks
onBeforeLogin: (config) => {
console.log("About to login");
},
onAfterLogin: (tokens) => {
console.log("Login successful");
},
onBeforeLogout: () => {
console.log("About to logout");
},
onAfterLogout: () => {
console.log("Logout complete");
},
// Additional Parameters
extraAuthParams: {
prompt: "login",
display: "popup"
},
extraTokenParams: {
client_secret: "secret" // Only for confidential clients
},
// Environment
environment: "browser", // Auto-detected: browser | node | react-native | worker
fetchFunction: fetch, // Custom fetch implementation
debug: true // Enable debug logging
});import { useState, useEffect } from 'react';
import ZenuxOAuth from 'zenuxs-oauth';
function useZenuxAuth(config) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [oauth] = useState(() => new ZenuxOAuth(config));
useEffect(() => {
setIsAuthenticated(oauth.isAuthenticated());
oauth.on('login', async (tokens) => {
setIsAuthenticated(true);
const userInfo = await oauth.getUserInfo();
setUser(userInfo);
});
oauth.on('logout', () => {
setIsAuthenticated(false);
setUser(null);
});
setLoading(false);
return () => {
oauth.off('login');
oauth.off('logout');
};
}, [oauth]);
return {
isAuthenticated,
user,
loading,
login: (options) => oauth.login(options),
logout: (options) => oauth.logout(options),
getTokens: () => oauth.getTokens()
};
}
// Usage in component
function App() {
const { isAuthenticated, user, loading, login, logout } = useZenuxAuth({
clientId: "your-client-id",
authServer: "https://api.auth.zenuxs.in",
redirectUri: window.location.origin + "/callback.html",
scopes: "openid profile email"
});
if (loading) return <div>Loading...</div>;
return (
<div>
{isAuthenticated ? (
<div>
<h1>Welcome, {user?.name}!</h1>
<button onClick={() => logout({ revokeTokens: true })}>
Logout
</button>
</div>
) : (
<button onClick={() => login({ popup: true })}>
Login with Zenuxs
</button>
)}
</div>
);
}import { ref, onMounted, onUnmounted } from 'vue';
import ZenuxOAuth from 'zenuxs-oauth';
export function useZenuxAuth(config) {
const isAuthenticated = ref(false);
const user = ref(null);
const loading = ref(true);
let oauth;
onMounted(() => {
oauth = new ZenuxOAuth(config);
isAuthenticated.value = oauth.isAuthenticated();
oauth.on('login', async (tokens) => {
isAuthenticated.value = true;
user.value = await oauth.getUserInfo();
});
oauth.on('logout', () => {
isAuthenticated.value = false;
user.value = null;
});
loading.value = false;
});
onUnmounted(() => {
if (oauth) {
oauth.off('login');
oauth.off('logout');
}
});
return {
isAuthenticated,
user,
loading,
login: (options) => oauth.login(options),
logout: (options) => oauth.logout(options)
};
}import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import ZenuxOAuth from 'zenuxs-oauth';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private oauth: any;
private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
private userSubject = new BehaviorSubject<any>(null);
public isAuthenticated$: Observable<boolean> = this.isAuthenticatedSubject.asObservable();
public user$: Observable<any> = this.userSubject.asObservable();
constructor() {
this.oauth = new ZenuxOAuth({
clientId: 'your-client-id',
authServer: 'https://api.auth.zenuxs.in',
redirectUri: window.location.origin + '/callback.html',
scopes: 'openid profile email'
});
this.isAuthenticatedSubject.next(this.oauth.isAuthenticated());
this.oauth.on('login', async (tokens: any) => {
this.isAuthenticatedSubject.next(true);
const user = await this.oauth.getUserInfo();
this.userSubject.next(user);
});
this.oauth.on('logout', () => {
this.isAuthenticatedSubject.next(false);
this.userSubject.next(null);
});
}
async login(options?: any): Promise<void> {
await this.oauth.login(options);
}
async logout(options?: any): Promise<void> {
await this.oauth.logout(options);
}
getTokens() {
return this.oauth.getTokens();
}
}| Feature | Zenuxs OAuth | Auth0-SPA | Firebase Auth | Hello.js | OAuth2-Client |
|---|---|---|---|---|---|
| Universal Support | β All platforms | β Browser only | β Browser only | ||
| PKCE Support | β Built-in | β Yes | β Yes | β No | |
| Popup Flow | β Native | β Yes | β No | β Yes | β No |
| Auto Token Refresh | β Configurable | β Yes | β Yes | β No | |
| Event System | β Comprehensive | β Good | β No | β No | |
| Zero Dependencies | β Yes | β No | β No | β Yes | β No |
| TypeScript | β Full support | β Yes | β Yes | β No | β Yes |
| Bundle Size | π’ ~15KB | π‘ ~50KB | π΄ ~150KB | π’ ~10KB | π‘ ~30KB |
| React Native | β Native | β No | β Separate pkg | β No | β No |
| Web Workers | β Yes | β No | β No | β No | β No |
| Custom Storage | β Flexible | β Fixed | β Fixed | ||
| Token Revocation | β Built-in | β Yes | β No | ||
| Session Export | β Yes | β No | β No | β No | β No |
| Learning Curve | π’ Low | π‘ Medium | π‘ Medium | π’ Low | π΄ High |
| Provider Lock-in | β None | π΄ Auth0 only | π΄ Firebase only | β None | |
| License | β MIT | β MIT | β MIT | β MIT |
- True Universal Support - One library for browser, Node.js, React Native, and Web Workers
- Zero Dependencies - No bloat, just pure OAuth functionality
- Developer Experience - Intuitive API with comprehensive events
- Flexibility - Works with any OAuth 2.0 provider, not locked to a specific service
- Modern Architecture - Built with PKCE, CSRF protection, and auto-refresh from the ground up
- Session Portability - Export/import sessions for cross-device authentication
- Lightweight - Only ~15KB minified + gzipped
const oauth = new ZenuxOAuth({
clientId: "your-client-id",
usePKCE: true // Always enabled by default
});const oauth = new ZenuxOAuth({
clientId: "your-client-id",
useCSRF: true, // Browser only
validateState: true // Verify state parameter
});// For web apps: Use sessionStorage (cleared on tab close)
storage: "sessionStorage"
// For SPAs with persistence: Use localStorage with caution
storage: "localStorage"
// For server-side: Always use memory storage
storage: "memory"await oauth.logout({
revokeTokens: true // Properly invalidate tokens
});oauth.on('tokenRefresh', (newTokens) => {
// Update your application state
updateAuthState(newTokens);
});
oauth.on('error', async (error) => {
if (error.code === 'TOKEN_REFRESH_FAILED') {
// Force re-login if refresh fails
await oauth.logout();
redirectToLogin();
}
});try {
const tokens = await oauth.login({
popup: true,
timeout: 300000 // 5 minutes timeout
});
} catch (error) {
if (error.code === 'LOGIN_TIMEOUT') {
console.log('Login took too long');
}
}try {
await oauth.login({ popup: true });
} catch (error) {
switch (error.code) {
case 'INVALID_CONFIG':
// Configuration validation failed
break;
case 'FETCH_UNAVAILABLE':
// Fetch API not available
break;
case 'POPUP_BLOCKED':
// Browser blocked popup window
alert('Please allow popups for this site');
break;
case 'AUTH_CANCELLED':
// User closed popup or cancelled authentication
console.log('User cancelled login');
break;
case 'LOGIN_TIMEOUT':
// Login process exceeded timeout
console.log('Login timeout');
break;
case 'STATE_MISMATCH':
// CSRF protection: state parameter mismatch
console.error('Security error detected');
break;
case 'NO_AUTH_CODE':
// Authorization code not received
break;
case 'TOKEN_EXCHANGE_FAILED':
// Failed to exchange code for tokens
break;
case 'TOKEN_REFRESH_FAILED':
// Failed to refresh access token
await oauth.logout();
break;
case 'NO_REFRESH_TOKEN':
// No refresh token available
break;
case 'NO_ACCESS_TOKEN':
// No access token available
break;
case 'USERINFO_FAILED':
// Failed to fetch user information
break;
case 'REVOKE_FAILED':
// Token revocation failed
break;
case 'INTROSPECT_FAILED':
// Token introspection failed
break;
default:
console.error('Unknown error:', error);
}
}// Global error handler
oauth.on('error', (error) => {
console.error('OAuth Error:', {
code: error.code,
message: error.message,
details: error.details,
environment: error.environment,
timestamp: error.timestamp
});
// Send to error tracking service
trackError(error);
});new ZenuxOAuth(config)login(options?)- Start OAuth flowhandleCallback(url)- Process OAuth callbacklogout(options?)- Logout user
getTokens()- Get current tokensisAuthenticated()- Check authentication statusisTokenExpired()- Check if token is expiredrefreshTokens()- Manually refresh tokensrevokeToken(token, tokenType)- Revoke specific token
getUserInfo()- Fetch user profileintrospectToken(token?)- Validate token
getSessionState()- Get current session stateexportSession()- Export session dataimportSession(data)- Import session data
on(event, handler)- Add event listeneroff(event, handler)- Remove event listener
getAuthenticatedFetch()- Get authenticated fetch functionupdateConfig(config)- Update configurationdestroy()- Cleanup resources
ZenuxOAuth.create(config)- Create new instanceZenuxOAuth.getInstance(config)- Get singleton instanceZenuxOAuth.destroyInstance()- Destroy singletonZenuxOAuth.createCallbackHandler(config)- Create callback handler
import ZenuxOAuth from 'zenuxs-oauth';
describe('ZenuxOAuth', () => {
let oauth;
beforeEach(() => {
oauth = new ZenuxOAuth({
clientId: 'test-client-id',
authServer: 'https://test-auth.example.com',
storage: 'memory'
});
});
afterEach(() => {
oauth.destroy();
});
test('should initialize correctly', () => {
expect(oauth).toBeDefined();
expect(oauth.isAuthenticated()).toBe(false);
});
test('should handle login flow', async () => {
const authData = await oauth.login();
expect(authData).toHaveProperty('url');
expect(authData).toHaveProperty('state');
expect(authData).toHaveProperty('codeVerifier');
});
test('should emit login event on successful authentication', (done) => {
oauth.on('login', (tokens) => {
expect(tokens).toHaveProperty('access_token');
done();
});
// Simulate login...
});
});We welcome contributions! Please see our Contributing Guide for details.
# Clone repository
git clone https://github.com/developers-rs5/zenuxs-oauth.git
cd zenuxs-oauth
# Install dependencies
npm install
# Run tests
npm test
# Build library
npm run build
# Run examples
npm run devMIT License Β© 2025 Zenuxs Team
Developed by Rishabh Sharma (rs)
- Documentation: https://docs.zenuxs.in
- GitHub: https://github.com/developers-rs5/zenuxs-oauth
- NPM: https://www.npmjs.com/package/zenuxs-oauth
- Discord: https://discord.zenuxs.in
- Issues: https://github.com/developers-rs5/zenuxs-oauth/issues
Need help? We're here for you:
- π Documentation: Check our comprehensive docs
- π¬ Discord: Join our community server
- π Issues: Report bugs on GitHub
- π§ Email: support@zenuxs.in
Special thanks to all contributors and the OAuth 2.0 community for making secure authentication accessible to everyone.
Made with β€οΈ by the Zenuxs Team