@vuer-ai/vuer-auth-client
v1.0.0
Published
A lightweight OIDC/OAuth 2.0 client with PKCE support for React applications
Readme
Vuer Auth Client
A lightweight OIDC/OAuth2 authentication client with PKCE support, powered by Zustand for React applications.
Features
- OIDC/OAuth2 with PKCE: Secure authorization code flow with PKCE
- Dual Auth Modes: Popup or redirect-based authentication
- Zustand Powered: Efficient state management with Zustand
- Type-Safe: Full TypeScript support with type inference
- React Hooks: Clean, intuitive hooks API (
useUserinfo,useAuth) - Framework Agnostic Core: Core client can be used without React
- Token Management: Automatic token storage and refresh support
- Lightweight: Minimal dependencies, tree-shakeable
Installation
pnpm add @vuer-ai/vuer-auth-client zustand react
# or
npm install @vuer-ai/vuer-auth-client zustand reactQuick Start
1. Create Auth Client
import { createAuthClient } from '@vuer-ai/vuer-auth-client';
const authClient = createAuthClient({
baseURL: 'https://your-auth-server.com',
clientId: 'your-client-id',
redirectUri: '/auth/callback',
});
export { authClient };2. Use in Components
import { authClient } from './auth';
import { AuthMode } from '@vuer-ai/vuer-auth-client';
function App() {
const { userinfo, isPending, error } = authClient.useUserinfo();
if (isPending) return <div>Loading...</div>;
if (!userinfo) return <LoginForm />;
return (
<div>
<h1>Welcome, {userinfo.name}!</h1>
<button onClick={() => authClient.signOut()}>Sign Out</button>
</div>
);
}
function LoginForm() {
const handleLogin = async () => {
// Popup mode - returns token directly
const token = await authClient.signIn({
authMode: AuthMode.Popup,
callbackUrl: '/dashboard',
});
if (token) {
console.log('Logged in successfully!');
}
};
const handleRedirectLogin = () => {
// Redirect mode - redirects to auth provider
authClient.signIn({
authMode: AuthMode.Redirect,
callbackUrl: '/dashboard',
});
};
return (
<div>
<button onClick={handleLogin}>Sign In (Popup)</button>
<button onClick={handleRedirectLogin}>Sign In (Redirect)</button>
</div>
);
}3. Handle Callback (Redirect Mode Only)
// pages/auth/callback/page.tsx
import { useEffect } from 'react';
import { authClient } from '@/auth';
export default function AuthCallback() {
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
if (code && state) {
authClient.getToken(code, state);
// Will automatically redirect to callbackUrl after success
}
}, []);
return <div>Completing sign in...</div>;
}API Reference
Configuration
interface AuthClientConfig {
/** Auth server base URL (e.g., 'https://auth.example.com') */
baseURL?: string;
/** OAuth2 client ID */
clientId?: string;
/** OAuth2 redirect URI (e.g., '/auth/callback') */
redirectUri?: string;
/** Additional fetch options */
fetchOptions?: RequestInit;
}Core Methods
signIn(auth)
Initiates the authentication flow.
// Popup mode - returns token directly
const token = await authClient.signIn({
authMode: AuthMode.Popup,
callbackUrl: '/dashboard',
});
// Redirect mode - redirects user to auth provider
authClient.signIn({
authMode: AuthMode.Redirect,
callbackUrl: '/dashboard',
});Parameters:
authMode:AuthMode.PopuporAuthMode.RedirectcallbackUrl: URL to redirect to after successful authentication
Returns:
- Popup mode:
Promise<Token | null> - Redirect mode:
null(performs redirect)
signOut(redirectUri?)
Signs out the user and clears tokens.
authClient.signOut(); // Redirects to '/'
authClient.signOut('/login'); // Redirects to '/login'getToken(code, state)
Exchanges authorization code for tokens (used in redirect mode callback).
await authClient.getToken(code, state);getUserinfo()
Fetches user information from the OIDC provider.
const user = await authClient.getUserinfo();
console.log(user.name, user.email);refreshToken()
Refreshes the access token using the refresh token.
const newToken = await authClient.refreshToken();token()
Gets the current token from storage.
const currentToken = authClient.token();
console.log(currentToken?.access_token);$fetch(path, init?)
Low-level fetch wrapper with error handling.
const { data, error } = await authClient.$fetch('/api/custom', {
method: 'POST',
body: JSON.stringify({ foo: 'bar' }),
});React Hooks
useUserinfo()
Hook for accessing user information with reactive state.
const { userinfo, isPending, isRefetching, error, refetch } = authClient.useUserinfo();Returns:
{
userinfo: User | null;
isPending: boolean; // Initial loading state
isRefetching: boolean; // Refetching state
error: Error | null;
refetch: () => Promise<void>;
}useAuth()
Convenience hook for authentication state and actions.
const { user, isAuthenticated, isPending, signIn, signOut, token, refetch } = authClient.useAuth();Returns:
{
user: User | null;
isAuthenticated: boolean;
isPending: boolean;
signIn: (auth) => Promise<Token | null>;
signOut: (redirectUri?: string) => void;
token: () => Token | null;
refetch: () => Promise<void>;
}Types
import type {
AuthClientConfig,
Session,
User,
Token,
AuthMode,
FetchError,
} from '@vuer-ai/vuer-auth-client';
interface User {
sub: string;
name?: string;
email?: string;
picture?: string;
given_name?: string;
family_name?: string;
email_verified?: string;
}
interface Token {
access_token: string;
refresh_token: string;
id_token: string;
token_type: string;
expires_in: number;
scope: string;
}
enum AuthMode {
Popup = 'popup',
Redirect = 'redirect'
}Usage Examples
Protected Route
import { Navigate } from 'react-router-dom';
import { authClient } from './auth';
function ProtectedRoute({ children }) {
const { user, isPending } = authClient.useAuth();
if (isPending) return <div>Loading...</div>;
if (!user) return <Navigate to="/login" />;
return children;
}
// Usage
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>Automatic Token Refresh
import { useEffect } from 'react';
import { authClient } from './auth';
function TokenRefreshManager() {
useEffect(() => {
const checkToken = async () => {
const token = authClient.token();
if (token) {
const expiresIn = token.expires_in * 1000; // Convert to ms
const now = Date.now();
const timeUntilExpiry = expiresIn - now;
// Refresh if expires in less than 5 minutes
if (timeUntilExpiry < 5 * 60 * 1000) {
await authClient.refreshToken();
}
}
};
// Check every minute
const interval = setInterval(checkToken, 60000);
checkToken(); // Initial check
return () => clearInterval(interval);
}, []);
return null;
}Error Handling
function LoginWithErrorHandling() {
const [error, setError] = useState<string | null>(null);
const handleLogin = async () => {
try {
const token = await authClient.signIn({
authMode: AuthMode.Popup,
callbackUrl: '/dashboard',
});
if (!token) {
setError('Failed to sign in. Please try again.');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
};
return (
<div>
<button onClick={handleLogin}>Sign In</button>
{error && <div className="error">{error}</div>}
</div>
);
}Custom API Calls
function UserProfile() {
const [profile, setProfile] = useState(null);
useEffect(() => {
const loadProfile = async () => {
const { data, error } = await authClient.$fetch('/api/profile');
if (!error) {
setProfile(data);
}
};
loadProfile();
}, []);
return <div>{profile?.bio}</div>;
}Popup vs Redirect Mode
Popup Mode (Recommended for SPAs):
- Better UX - no full page reload
- Returns token directly in promise
- Uses
window.postMessagefor communication - Requires popup blocker to be disabled
const token = await authClient.signIn({
authMode: AuthMode.Popup,
callbackUrl: '/dashboard',
});Redirect Mode (Traditional):
- Full page redirect to auth provider
- Better compatibility with strict security policies
- Requires callback page to handle token exchange
// Login page
authClient.signIn({
authMode: AuthMode.Redirect,
callbackUrl: '/dashboard',
});
// Callback page
useEffect(() => {
const params = new URLSearchParams(window.location.search);
authClient.getToken(params.get('code')!, params.get('state')!);
}, []);Architecture
Inspired by better-auth's client design:
┌─────────────────────────────────────┐
│ createAuthClient() │
│ │
│ ┌──────────────────────────────┐ │
│ │ Core Client │ │
│ │ - OIDC/OAuth2 with PKCE │ │
│ │ - Token management │ │
│ │ - $fetch wrapper │ │
│ └──────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────┐ │
│ │ Zustand Store │ │
│ │ - User info state │ │
│ │ - Loading states │ │
│ └──────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────┐ │
│ │ React Hooks │ │
│ │ - useUserinfo() │ │
│ │ - useAuth() │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘See ARCHITECTURE.md for detailed architecture documentation.
Security
PKCE (Proof Key for Code Exchange)
This library implements OAuth 2.0 Authorization Code Flow with PKCE:
- Code Verifier: Random 64-character string
- Code Challenge: SHA-256 hash of verifier
- State Parameter: Random 32-character string for CSRF protection
- Secure Storage: Tokens in localStorage, PKCE params in sessionStorage
Best Practices
- Use HTTPS: Always use HTTPS in production
- Validate State: State parameter is automatically validated
- Token Storage: Tokens are stored securely in localStorage
- Origin Validation: Popup mode validates message origins
- Token Refresh: Implement automatic token refresh before expiry
TypeScript
Full type inference works out of the box:
const authClient = createAuthClient({
baseURL: 'https://auth.example.com',
clientId: 'my-client-id',
redirectUri: '/callback',
});
// All methods are fully typed
type Client = typeof authClient;
// Client: {
// signIn: (auth: {...}) => Promise<Token | null>
// getUserinfo: () => Promise<User | null>
// useUserinfo: () => {...}
// ...
// }Comparison with better-auth
Similarities:
- Hooks attached to client object pattern
- Full TypeScript support with type inference
- Factory pattern API
Differences:
- Focus: OIDC/OAuth2 client-side only (better-auth includes backend)
- State Management: Zustand instead of nanostores
- Simplicity: No plugin system, focused on OAuth2/OIDC
- PKCE: Built-in PKCE support for browser-based apps
- Popup Mode: Native popup authentication support
License
MIT
Contributing
Contributions welcome! Please submit issues and PRs to the repository.
