React authentication middleware for University of Jaffna Auth Service with OAuth 2.0 + PKCE, httpOnly cookies, time-bound roles and permissions
npm install @uoj-lk/auth-reactReact authentication middleware for University of Jaffna Auth Service with OAuth 2.0 + PKCE, httpOnly cookies, time-bound roles, and permissions.


> 📚 Complete Documentation Index | OAuth Quick Start | Full OAuth Guide | Permission System Update
- ⏱️ Request Timeouts: API calls now time out by default to avoid hanging UIs
- 🌐 Smarter Errors: isNetworkError, isTimeoutError, and getNetworkErrorMessage utilities for clearer UX
- 🛡️ Runtime Validation: Auth config validated at runtime with friendly errors
- 🧭 Error Boundary: prevents auth failures from crashing the app
- 🔎 Dev Diagnostics: Optional dev logging (enableDevLogging) to trace auth flows locally
- 🧩 DX Polishing: Components have display names; TypeScript defs updated for new helpers
See IMPROVEMENTS_INDEX.md for the full v3.2.0 changeset
- 🚀 OAuth 2.0 + PKCE - Complete Authorization Code flow for SPAs
- 🔐 Secure Public Client - No client secrets in browser, PKCE required
- ⚡ Drop-in Components - and ready to use
- 🎣 OAuth Hooks - startOAuthFlow() and handleOAuthCallback() in useAuth()
- 🛠️ PKCE Utilities - Code verifier/challenge generation with fallbacks
- 🔒 CSRF Protection - Automatic state validation
- 🌐 Works Anywhere - Web Crypto with js-sha256 fallback for non-secure contexts
- 🔐 Complete Authentication - Session-based login, OAuth, automatic token refresh
- ⏰ Time-Bound Roles - Support for roles with start/end dates
- 🏢 Organization Context - Multi-organization role management
- 🎯 Permission-Based UI - Show/hide components based on permissions
- 🛡️ 8+ Middleware Components - Ready-to-use access control components
- 📊 Role Expiry Tracking - Automatic warnings for expiring roles
- 🔄 Flexible Permission Checks - Support for multiple formats and combinations
- 🎨 TypeScript Support - Full type definitions included
- 🔙 Backward Compatible - All v2.x features preserved
``bash`
npm install @uoj-lk/auth-react
This package requires the following peer dependencies:
`bash`
npm install react react-dom react-router-dom
> 📚 Complete Documentation Index →
Quick links:
- OAuth Quick Start - 30-second setup
- OAuth Complete Guide - In-depth OAuth guide
- Publishing Guide - Maintain & publish package
#### 1. Setup AuthProvider
`jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import {
AuthProvider,
OAuthButton,
OAuthCallback,
ProtectedRoute,
AuthErrorBoundary,
} from "@uoj-lk/auth-react";
import Dashboard from "./pages/Dashboard";
// Configure authentication
const authConfig = {
apiUrl: "https://api.example.com/api",
clientId: "your-client-id",
requestTimeout: 10000, // Optional: abort slow requests (ms)
enableDevLogging: process.env.NODE_ENV === "development",
// No clientSecret for public clients (SPAs)
};
function App() {
return (
{/ Login with OAuth /}
element={
Login
Sign in with OAuth
}
/>
{/ OAuth Callback Handler /}
element={
}
/>
{/ Protected Dashboard /}
element={
}
/>
);
}
export default App;
`
#### 2. Use Auth in Your Components
`jsx
import { useAuth } from "@uoj-lk/auth-react";
function Dashboard() {
const { user, logout, hasPermission, hasRole } = useAuth();
return (
{hasRole("admin") &&
#### Handle Network/Timeout Errors
`jsx
import {
isNetworkError,
isTimeoutError,
getNetworkErrorMessage,
} from "@uoj-lk/auth-react";async function saveProfile(payload) {
try {
// your API call here
} catch (error) {
if (isTimeoutError(error)) return alert("Request took too long");
if (isNetworkError(error)) return alert("You appear offline");
return alert(getNetworkErrorMessage(error));
}
}
`$3
#### 1. Wrap Your App with AuthProvider
`jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { AuthProvider, ProtectedRoute } from "@uoj-lk/auth-react";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";// Configure authentication
const authConfig = {
apiUrl: "https://your-auth-api.com/api",
clientId: "your-client-id",
clientSecret: "your-client-secret", // For confidential clients
};
function App() {
return (
} />
path="/dashboard"
element={
}
/>
);
}
export default App;
`#### 2. Use Auth in Your Components
`jsx
import { useAuth } from "@uoj-lk/auth-react";function Login() {
const { login } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
const { success } = await login({
username: e.target.username.value,
password: e.target.password.value,
});
if (success) {
// Redirect to dashboard
}
};
return
;
}function Dashboard() {
const { user, logout, hasPermission } = useAuth();
return (
Welcome, {user?.firstName}!
{hasPermission("course", "create") && }
);
}
`🔓 OAuth 2.0 Components (v3.0.0)
$3
Initiates OAuth 2.0 Authorization Code + PKCE flow.
`jsx
import { OAuthButton } from "@uoj-lk/auth-react"; authUrl="https://auth.example.com/oauth/authorize"
scope="openid profile email"
callbackPath="/oauth/callback"
className="btn btn-primary"
>
Sign in with OAuth
;
`Props:
| Prop | Type | Default | Description |
| -------------- | ----------- | ------------------------ | -------------------------- |
|
authUrl | string | Required | Authorization endpoint URL |
| scope | string | 'openid profile email' | OAuth scopes |
| callbackPath | string | '/oauth/callback' | Callback path |
| extraParams | object | {} | Additional OAuth params |
| className | string | 'btn btn-primary' | CSS classes |
| children | ReactNode | 'Sign in with OAuth' | Button content |
| disabled | boolean | false | Disable button |$3
Handles OAuth callback, validates state, exchanges code for tokens.
`jsx
import { OAuthCallback } from "@uoj-lk/auth-react"; tokenUrl="https://api.example.com/api/oauth/token"
successPath="/dashboard"
errorPath="/login"
onSuccess={(user) => console.log("Logged in:", user)}
onError={(err) => console.error("Login failed:", err)}
/>;
`Props:
| Prop | Type | Default | Description |
| ------------------ | ----------- | ------------------- | ------------------- |
|
tokenUrl | string | Required | Token endpoint URL |
| callbackPath | string | '/oauth/callback' | Callback path |
| successPath | string | '/dashboard' | Redirect on success |
| errorPath | string | '/login' | Redirect on error |
| onSuccess | function | - | Success callback |
| onError | function | - | Error callback |
| loadingComponent | ReactNode | Default spinner | Custom loading UI |
| errorComponent | function | Default alert | Custom error UI |🔐 Middleware Components
$3
Requires user to be authenticated.
`jsx
import { ProtectedRoute } from "@uoj-lk/auth-react"; path="/dashboard"
element={
}
/>;
`$3
Requires user to have a specific role.
`jsx
import { RequireRole } from "@uoj-lk/auth-react";
;
`$3
Requires user to have a specific permission.
`jsx
import { RequirePermission } from "@uoj-lk/auth-react";// Method 1: Separate resource and action
// Method 2: Combined permission string
`$3
Requires user to have a role in a specific organization.
`jsx
import { RequireRoleInOrganization } from "@uoj-lk/auth-react";
;
`$3
Requires user to have at least one of the specified permissions.
`jsx
import { RequireAnyPermission } from "@uoj-lk/auth-react";
;
`$3
Requires user to have all of the specified permissions.
`jsx
import { RequireAllPermissions } from "@uoj-lk/auth-react";
;
`$3
Requires user to belong to a specific organization.
`jsx
import { RequireOrganization } from "@uoj-lk/auth-react";
;
`$3
Custom condition-based rendering.
`jsx
import { ConditionalRender } from "@uoj-lk/auth-react"; condition={({ hasPermission, hasRole }) =>
hasPermission("report:generate") && hasRole("Admin")
}
>
;
`🎯 useAuth Hook
The
useAuth hook provides access to authentication state and helper functions.$3
`typescript
const {
// State
user, // Current user object
loading, // Loading state (initial auth check)
permissionsLoading, // NEW v3.1: Permissions loading state
error, // Error message
isAuthenticated, // Boolean flag // Actions
login, // Login function (session-based)
logout, // Logout function
// OAuth 2.0 (v3.0.0)
startOAuthFlow, // Start OAuth authorization
handleOAuthCallback, // Handle OAuth callback
// Role Checks
hasRole, // Check if user has a role
hasRoleInOrganization, // Check role in specific org
getActiveRoles, // Get all active roles
// Permission Checks
hasPermission, // Check single permission
hasAnyPermission, // Check if has any of multiple
hasAllPermissions, // Check if has all of multiple
// Organization
belongsToOrganization, // Check org membership
getOrganizations, // Get all organizations
getPermissionsForOrganization, // Get org permissions
// Time-Bound Roles
calculateDaysRemaining, // Calculate days to expiry
getExpiringRoles, // Get roles expiring soon
// Error Handling (NEW v3.1)
parseError, // Parse API error details
formatPermissionError, // Format 403 error message
isPermissionError, // Check if error is 403
isAuthenticationError, // Check if error is 401
getErrorMessage, // Get user-friendly message
} = useAuth();
`$3
#### OAuth Login (v3.0.0)
`jsx
const { startOAuthFlow } = useAuth();const handleOAuthLogin = async () => {
await startOAuthFlow({
authUrl: "https://auth.example.com/oauth/authorize",
scope: "openid profile email",
callbackPath: "/oauth/callback",
});
// User will be redirected to authorization server
};
`#### Session Login
`jsx
const { login } = useAuth();const handleSubmit = async (e) => {
e.preventDefault();
const result = await login({ username, password });
if (result.success) {
navigate("/dashboard");
} else {
setError(result.error);
}
};
`#### Check Permissions
`jsx
const { hasPermission, hasAnyPermission } = useAuth();// Single permission
if (hasPermission("course", "create")) {
// Show create button
}
// Alternative format
if (hasPermission("course:create")) {
// Show create button
}
// Multiple permissions (any)
if (hasAnyPermission(["course:create", "course:update"])) {
// Show editor
}
`#### Role Expiry Tracking
`jsx
const { getExpiringRoles, calculateDaysRemaining } = useAuth();const expiringRoles = getExpiringRoles(30); // Roles expiring in 30 days
expiringRoles.forEach((role) => {
const daysLeft = calculateDaysRemaining(role.endDate);
console.log(
${role.name} expires in ${daysLeft} days);
});
`🚨 Error Handling (v3.1.0)
$3
Handle API errors with permission-aware messaging:
`jsx
import { useErrorHandler } from "@uoj-lk/auth-react";function MyComponent() {
const { handleError, handlePermissionError, isPermissionError } =
useErrorHandler();
const deleteRole = async (roleId) => {
try {
await api.deleteRole(roleId);
} catch (error) {
if (isPermissionError(error)) {
// Handle 403 - show detailed permission requirements
const permError = handlePermissionError(error);
console.log(permError.formattedMessage);
// "Insufficient permissions. Required permissions: roles:manage"
} else {
// Handle other errors
const errorInfo = handleError(error);
console.log(errorInfo.message);
}
}
};
}
`$3
Check permissions with fallback support:
`jsx
import { usePermissionCheck } from "@uoj-lk/auth-react";const { canAccess } = usePermissionCheck();
// Check permission with role fallback
if (canAccess("course:update", "ADMIN")) {
// Show edit button if has permission OR is admin
}
// Check multiple permissions
if (canAccess(["course:create", "course:update"])) {
// Show editor if has ANY permission
}
`$3
Simple access to authentication state:
`jsx
import { useAuthState } from "@uoj-lk/auth-react";const { isReady, isLoading, isPermissionsLoading, isAuthenticated } =
useAuthState();
if (!isReady) {
return ; // Wait for both auth and permissions
}
// Safe to check permissions now
`$3
Display formatted permission errors:
`jsx
import { PermissionErrorDisplay } from "@uoj-lk/auth-react";const [error, setError] = useState(null);
error={error}
onDismiss={() => setError(null)}
dismissable={true}
/>;
`$3
Handle async actions with automatic error display:
`jsx
import { AsyncActionWithErrorHandling } from "@uoj-lk/auth-react"; onAction={async () => await api.deleteRole(roleId)}
actionLabel="Delete Role"
onSuccess={() => toast.success("Role deleted")}
/>;
`$3
Forms with permission-aware error handling:
`jsx
import { FormWithPermissionHandling } from "@uoj-lk/auth-react"; onSubmit={handleSubmit}
successMessage="Role created successfully"
>
;
`�️ OAuth Utilities (v3.0.0)
For advanced OAuth use cases, you can use the low-level utilities directly:
$3
`jsx
import {
generateCodeVerifier,
computeCodeChallengeS256,
generateState,
storePKCE,
loadPKCE,
clearPKCE,
} from "@uoj-lk/auth-react";// Generate PKCE parameters
const verifier = generateCodeVerifier(64); // 43-128 chars
const challenge = await computeCodeChallengeS256(verifier);
const state = generateState();
// Store for callback validation
storePKCE({ codeVerifier: verifier, state });
// Later, in callback
const { codeVerifier, state: storedState } = loadPKCE();
// After token exchange
clearPKCE();
`$3
`jsx
import {
buildAuthorizeUrl,
parseCallbackParams,
validateState,
exchangeCodeForTokens,
getRedirectUri,
} from "@uoj-lk/auth-react";// Build authorization URL
const authUrl = buildAuthorizeUrl({
authUrl: "https://auth.example.com/oauth/authorize",
clientId: "your-client-id",
redirectUri: getRedirectUri("/oauth/callback"),
codeChallenge: challenge,
state,
scope: "openid profile email",
});
// Parse callback parameters
const { code, state, error, errorDescription } = parseCallbackParams(
window.location.search
);
// Validate state (CSRF protection)
if (!validateState(state, storedState)) {
console.error("State mismatch - possible CSRF attack");
}
// Exchange code for tokens
await exchangeCodeForTokens({
tokenUrl: "https://api.example.com/api/oauth/token",
code,
clientId: "your-client-id",
redirectUri: getRedirectUri("/oauth/callback"),
codeVerifier,
httpClient: axiosInstance,
});
`Security Features:
- ✅ PKCE: Prevents authorization code interception
- ✅ State Validation: CSRF protection
- ✅ Secure RNG: Uses
crypto.getRandomValues() when available
- ✅ Fallback Support: Works in non-secure contexts (development)�📊 User Object Structure
`typescript
interface User {
id: number;
username: string;
email: string;
firstName: string;
lastName: string;
phone?: string;
isLdapUser: boolean;
isActive: boolean; // Simple role names (backward compatibility)
roles: string[];
// Full role details with time-bound support
roleDetails: RoleDetail[];
// Aggregated permissions from all active roles
permissions: Permission[];
// Organizations user belongs to
organizations: Organization[];
}
`⚙️ Configuration
$3
| Prop | Type | Required | Default | Description |
| ---------- | --------- | -------- | ------- | -------------------------------------------------------- |
|
children | ReactNode | Yes | - | Child components |
| config | object | No | - | Configuration object with apiUrl, clientId, clientSecret |
| authAPI | object | No | - | Custom API client (for advanced usage) |$3
| Property | Type | Required | Default | Description |
| -------------- | -------- | -------- | ------- | ---------------------- |
|
apiUrl | string | Yes | - | Backend API URL |
| clientId | string | No | - | OAuth client ID |
| clientSecret | string | No | - | OAuth client secret |
| onLogout | function | No | - | Custom logout callback |$3
If you're using Vite, you can set these in
.env:`env
VITE_API_URL=https://your-auth-api.com/api
VITE_CLIENT_ID=your-client-id
VITE_CLIENT_SECRET=your-client-secret
`Then use them in your config:
`jsx
const authConfig = {
apiUrl: import.meta.env.VITE_API_URL || "http://localhost:3000/api",
clientId: import.meta.env.VITE_CLIENT_ID,
clientSecret: import.meta.env.VITE_CLIENT_SECRET,
};{/ Your app /} ;
`$3
#### Custom Logout Callback
`jsx
const authConfig = {
apiUrl: "https://your-auth-api.com/api",
clientId: "your-client-id",
clientSecret: "your-client-secret",
onLogout: () => {
console.log("User logged out");
// Custom cleanup logic
},
};
`#### Custom API Client
For advanced use cases, you can provide a custom API client:
`jsx
import { createAuthAPI } from "@uoj-lk/auth-react";const customAPI = createAuthAPI({
apiUrl: "https://your-auth-api.com/api",
clientId: "your-client-id",
clientSecret: "your-client-secret",
onLogout: () => {
// Custom logout logic
},
});
{/ Your app /} ;
`🔄 Automatic Token Refresh
The package automatically handles token refresh when the access token expires. The API client includes an interceptor that:
1. Detects 401 responses
2. Attempts to refresh the token using the refresh token
3. Retries the original request with the new token
4. Redirects to login if refresh fails
🎨 Custom Fallbacks
All middleware components support custom fallback UI:
`jsx
resource="course"
action="delete"
fallback={
You don't have permission to delete courses. Contact your administrator.
}
>
`📚 Advanced Examples
$3
`jsx
import { ConditionalRender } from "@uoj-lk/auth-react";function Navigation() {
return (
hasRole("Admin")}>
Admin Panel
);
}
`$3
`jsx
import { useAuth } from "@uoj-lk/auth-react";function RoleExpiryAlert() {
const { getExpiringRoles, calculateDaysRemaining } = useAuth();
const expiringRoles = getExpiringRoles(30);
if (expiringRoles.length === 0) return null;
return (
Warning! You have {expiringRoles.length} role(s) expiring
soon:
{expiringRoles.map((role, idx) => (
{role.name} in {role.organization?.name} -
{calculateDaysRemaining(role.endDate)} days remaining
))}
);
}
`$3
`jsx
import { useAuth } from "@uoj-lk/auth-react";function OrganizationSelector() {
const { getOrganizations, user } = useAuth();
const organizations = getOrganizations();
return (
);
}
`🔒 Security Best Practices
$3
PKCE Required: The package automatically implements PKCE (Proof Key for Code Exchange) for all OAuth flows to prevent authorization code interception attacks.
State Validation: Every OAuth request includes a random state parameter that is validated on callback to prevent CSRF attacks.
No Client Secrets: Public clients (SPAs) should never store client secrets. Use
clientId only in AuthProvider config.HTTPS in Production: OAuth requires HTTPS in production. The package works with HTTP in development but shows warnings.
`jsx
// ✅ Good - Public client (SPA)
const authConfig = {
apiUrl: "https://api.example.com/api",
clientId: "your-client-id",
// No clientSecret for SPAs
};// ❌ Bad - Secret exposed in browser
const authConfig = {
apiUrl: "https://api.example.com/api",
clientId: "your-client-id",
clientSecret: "secret-key", // NEVER in browser!
};
`$3
Never hardcode sensitive credentials in your code. Always use environment variables:
`jsx
// ✅ Good - Using environment variables
const authConfig = {
apiUrl: import.meta.env.VITE_API_URL,
clientId: import.meta.env.VITE_CLIENT_ID,
clientSecret: import.meta.env.VITE_CLIENT_SECRET,
};// ❌ Bad - Hardcoded credentials
const authConfig = {
apiUrl: "https://api.example.com",
clientId: "my-client-id",
clientSecret: "my-secret-key", // NEVER DO THIS!
};
`$3
The package uses httpOnly cookies for token storage (when backend supports it) for enhanced security. Tokens are never exposed to JavaScript, preventing XSS attacks.
For OAuth flows, tokens are automatically stored in httpOnly cookies by the backend after successful authorization.
$3
Always use HTTPS in production to prevent man-in-the-middle attacks:
`jsx
const authConfig = {
apiUrl: "https://your-api.com/api", // ✅ HTTPS
// apiUrl: "http://your-api.com/api", // ❌ HTTP - Never in production!
};
`$3
The package automatically sanitizes error messages. API errors are caught and generic messages are shown to users to prevent information leakage.
$3
Configure your CSP headers to restrict API calls:
`
Content-Security-Policy: connect-src 'self' https://your-auth-api.com;
`$3
Always validate user input before passing to the login function:
`jsx
const handleLogin = async (credentials) => {
// Validate credentials
if (!credentials.username || !credentials.password) {
setError("Username and password are required");
return;
} if (credentials.username.length < 3) {
setError("Invalid username format");
return;
}
// Proceed with login
const result = await login(credentials);
// ...
};
`$3
Implement rate limiting on your authentication endpoints to prevent brute-force attacks. The package doesn't include rate limiting - this must be handled by your backend.
$3
Log authentication events (login, logout, failed attempts) on your backend for security auditing. Never log credentials or tokens.
🐛 Troubleshooting
$3
#### "crypto.subtle is undefined"
Cause: Web Crypto API not available in non-secure context (http://)
Solution: This is normal for development. The package automatically falls back to js-sha256. For production, use HTTPS.
#### "State mismatch - possible CSRF attack"
Cause: State parameter doesn't match stored value
Solution:
- Don't refresh the page during OAuth flow
- Clear browser cache and try again
- Check that callback URL is registered correctly
#### "Missing authorization code"
Cause: Authorization was denied or redirect URI not registered
Solution:
- Check backend logs for authorization errors
- Verify redirect URI exactly matches backend registration
- Ensure user approved the authorization request
#### "Invalid client_id"
Cause: Client ID not configured or doesn't match backend
Solution: Verify
clientId in AuthProvider config matches the client registered in backend.$3
$3
Make sure
AuthProvider wraps all components that use useAuth:`jsx
// ❌ Wrong
} />
...
// ✅ Correct
} />
`$3
Ensure your backend returns a refresh token on login and has a
/auth/refresh endpoint.$3
Check that your backend returns the enriched user object with:
-
roleDetails (array of role objects with permissions)
- permissions (aggregated permission array)
- organizations (user's organizations)$3
This error occurs when AuthProvider doesn't have a valid API URL. Fix by:
`jsx
// Option 1: Provide config prop
// Option 2: Set environment variable
// In .env file:
VITE_API_URL=https://your-api.com/api
`🛡️ Production Checklist
Before deploying to production, ensure:
- [ ] All sensitive credentials are in environment variables (not hardcoded)
- [ ] Using HTTPS for all API communication
- [ ] Environment variables are properly configured in your deployment platform
- [ ] Backend has rate limiting enabled on authentication endpoints
- [ ] Backend implements proper CORS policies
- [ ] Audit logging is enabled on backend
- [ ] Token expiration times are appropriate for your use case
- [ ] CSP headers are configured to restrict API calls
- [ ] Error messages don't expose sensitive information
- [ ] Authentication events are monitored and alerted
� Migration Guide
$3
Good news: No breaking changes! All v2.x code continues to work.
#### What's New
`jsx
// v2.x - Still works in v3.0.0
const { user, login, logout, hasRole, hasPermission } = useAuth();// v3.0.0 - New OAuth features available
const {
user,
login,
logout,
hasRole,
hasPermission,
startOAuthFlow, // NEW
handleOAuthCallback, // NEW
} = useAuth();
`#### Adding OAuth to Existing App
1. Install v3.0.0
`bash
npm install @uoj-lk/auth-react@3.0.0
`2. Add OAuth routes (your existing session login still works)
`jsx
import { OAuthButton, OAuthCallback } from "@uoj-lk/auth-react";
{/ Existing session login - still works /}
} />
{/ New OAuth option - add alongside /}
path="/oauth/start"
element={OAuth Login }
/>
} />
;
`3. Update config (optional - remove clientSecret for public clients)
`jsx
// v2.x config
const authConfig = {
apiUrl: "...",
clientId: "...",
clientSecret: "...", // Remove for SPAs
}; // v3.0.0 config (for OAuth public clients)
const authConfig = {
apiUrl: "...",
clientId: "...",
// No clientSecret for OAuth SPAs
};
`That's it! Your existing session-based login continues working, and you can now add OAuth as an alternative login method.
�📄 License
MIT License - see LICENSE file for details.
🤝 Contributing
Contributions are welcome! Please open an issue or submit a pull request.
📧 Support
For issues and questions:
- GitHub Issues: https://github.com/UoJ-LK/myuoj-auth/issues
- Documentation: https://github.com/UoJ-LK/myuoj-auth
🔗 Related Packages
-
@uoj-lk/auth-node` - Node.js/Express backend middleware (coming soon)---
Made with ❤️ by University of Jaffna