JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 6
  • Score
    100M100P100Q46783F
  • License MIT

React authentication middleware for University of Jaffna Auth Service with httpOnly cookies, time-bound roles and permissions

Package Exports

  • @uoj-lk/auth-react
  • @uoj-lk/auth-react/package.json

Readme

@uoj-lk/auth-react

React authentication middleware for University of Jaffna Auth Service with time-bound roles, permissions, and organization context.

npm version License: MIT

✨ Features

  • 🔐 Complete Authentication Flow - Login, logout, 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

📦 Installation

npm install @uoj-lk/auth-react

Peer Dependencies

This package requires the following peer dependencies:

npm install react react-dom react-router-dom

🚀 Quick Start

1. Wrap Your App with AuthProvider

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",
};

function App() {
  return (
    <Router>
      <AuthProvider config={authConfig}>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route
            path="/dashboard"
            element={
              <ProtectedRoute>
                <Dashboard />
              </ProtectedRoute>
            }
          />
        </Routes>
      </AuthProvider>
    </Router>
  );
}

export default App;

2. Use Auth in Your Components

import { useAuth } from "@uoj-lk/auth-react";

function Dashboard() {
  const { user, logout, hasPermission } = useAuth();

  return (
    <div>
      <h1>Welcome, {user?.firstName}!</h1>
      <button onClick={logout}>Logout</button>

      {hasPermission("course", "create") && <button>Create Course</button>}
    </div>
  );
}

🔐 Middleware Components

ProtectedRoute

Requires user to be authenticated.

import { ProtectedRoute } from "@uoj-lk/auth-react";

<Route
  path="/dashboard"
  element={
    <ProtectedRoute redirectTo="/login">
      <Dashboard />
    </ProtectedRoute>
  }
/>;

RequireRole

Requires user to have a specific role.

import { RequireRole } from "@uoj-lk/auth-react";

<RequireRole role="Admin">
  <AdminPanel />
</RequireRole>;

RequirePermission

Requires user to have a specific permission.

import { RequirePermission } from "@uoj-lk/auth-react";

// Method 1: Separate resource and action
<RequirePermission resource="course" action="create">
  <CreateCourseButton />
</RequirePermission>

// Method 2: Combined permission string
<RequirePermission permission="course:create">
  <CreateCourseButton />
</RequirePermission>

RequireRoleInOrganization

Requires user to have a role in a specific organization.

import { RequireRoleInOrganization } from "@uoj-lk/auth-react";

<RequireRoleInOrganization role="Instructor" organizationId={1}>
  <CourseList />
</RequireRoleInOrganization>;

RequireAnyPermission

Requires user to have at least one of the specified permissions.

import { RequireAnyPermission } from "@uoj-lk/auth-react";

<RequireAnyPermission permissions={["course:create", "course:update"]}>
  <CourseEditor />
</RequireAnyPermission>;

RequireAllPermissions

Requires user to have all of the specified permissions.

import { RequireAllPermissions } from "@uoj-lk/auth-react";

<RequireAllPermissions permissions={["course:create", "student:read"]}>
  <FullCourseManager />
</RequireAllPermissions>;

RequireOrganization

Requires user to belong to a specific organization.

import { RequireOrganization } from "@uoj-lk/auth-react";

<RequireOrganization organizationId={1}>
  <DepartmentDashboard />
</RequireOrganization>;

ConditionalRender

Custom condition-based rendering.

import { ConditionalRender } from "@uoj-lk/auth-react";

<ConditionalRender
  condition={({ hasPermission, hasRole }) =>
    hasPermission("report:generate") && hasRole("Admin")
  }
>
  <AdvancedReports />
</ConditionalRender>;

🎯 useAuth Hook

The useAuth hook provides access to authentication state and helper functions.

Available Properties

const {
  // State
  user, // Current user object
  loading, // Loading state
  error, // Error message
  isAuthenticated, // Boolean flag

  // Actions
  login, // Login function
  logout, // Logout function

  // 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
} = useAuth();

Examples

Login

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

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

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`);
});

📊 User Object Structure

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

AuthProvider Props

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)

Config Object

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

Environment Variables

If you're using Vite, you can set these in .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:

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,
};

<AuthProvider config={authConfig}>{/* Your app */}</AuthProvider>;

Advanced Configuration

Custom Logout Callback

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:

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
  },
});

<AuthProvider authAPI={customAPI}>{/* Your app */}</AuthProvider>;

🔄 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:

<RequirePermission
  resource="course"
  action="delete"
  fallback={
    <div className="alert alert-danger">
      You don't have permission to delete courses. Contact your administrator.
    </div>
  }
>
  <DeleteButton />
</RequirePermission>

📚 Advanced Examples

Conditional Navigation Menu

import { ConditionalRender } from "@uoj-lk/auth-react";

function Navigation() {
  return (
    <nav>
      <ConditionalRender
        condition={({ hasPermission }) => hasPermission("course:read")}
      >
        <NavItem to="/courses">My Courses</NavItem>
      </ConditionalRender>

      <ConditionalRender condition={({ hasRole }) => hasRole("Admin")}>
        <NavItem to="/admin">Admin Panel</NavItem>
      </ConditionalRender>
    </nav>
  );
}

Role Expiry Warning

import { useAuth } from "@uoj-lk/auth-react";

function RoleExpiryAlert() {
  const { getExpiringRoles, calculateDaysRemaining } = useAuth();
  const expiringRoles = getExpiringRoles(30);

  if (expiringRoles.length === 0) return null;

  return (
    <div className="alert alert-warning">
      <strong>Warning!</strong> You have {expiringRoles.length} role(s) expiring
      soon:
      <ul>
        {expiringRoles.map((role, idx) => (
          <li key={idx}>
            {role.name} in {role.organization?.name} -
            {calculateDaysRemaining(role.endDate)} days remaining
          </li>
        ))}
      </ul>
    </div>
  );
}

Organization Switcher

import { useAuth } from "@uoj-lk/auth-react";

function OrganizationSelector() {
  const { getOrganizations, user } = useAuth();
  const organizations = getOrganizations();

  return (
    <select>
      {organizations.map((org) => (
        <option key={org.id} value={org.id}>
          {org.name} ({org.code})
        </option>
      ))}
    </select>
  );
}

� Security Best Practices

Environment Variables

Never hardcode sensitive credentials in your code. Always use environment variables:

// ✅ 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!
};

Token Storage

The package stores tokens in localStorage. For production applications, consider:

  • Using httpOnly cookies for enhanced security (requires backend support)
  • Implementing token rotation strategies
  • Adding token expiration monitoring
  • Using secure, same-site cookie policies

HTTPS Only

Always use HTTPS in production to prevent man-in-the-middle attacks:

const authConfig = {
  apiUrl: "https://your-api.com/api", // ✅ HTTPS
  // apiUrl: "http://your-api.com/api", // ❌ HTTP - Never in production!
};

Error Handling

The package automatically sanitizes error messages. API errors are caught and generic messages are shown to users to prevent information leakage.

Content Security Policy (CSP)

Configure your CSP headers to restrict API calls:

Content-Security-Policy: connect-src 'self' https://your-auth-api.com;

Input Validation

Always validate user input before passing to the login function:

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);
  // ...
};

Rate Limiting

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.

Audit Logging

Log authentication events (login, logout, failed attempts) on your backend for security auditing. Never log credentials or tokens.

�🐛 Troubleshooting

"useAuth must be used within AuthProvider"

Make sure AuthProvider wraps all components that use useAuth:

// ❌ Wrong
<BrowserRouter>
  <Routes>
    <Route path="/dashboard" element={<Dashboard />} />
  </Routes>
  <AuthProvider>...</AuthProvider>
</BrowserRouter>

// ✅ Correct
<BrowserRouter>
  <AuthProvider>
    <Routes>
      <Route path="/dashboard" element={<Dashboard />} />
    </Routes>
  </AuthProvider>
</BrowserRouter>

Token Refresh Not Working

Ensure your backend returns a refresh token on login and has a /auth/refresh endpoint.

Permissions Not Showing

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)

"apiUrl is required" Error

This error occurs when AuthProvider doesn't have a valid API URL. Fix by:

// Option 1: Provide config prop
<AuthProvider config={{ apiUrl: "https://your-api.com/api" }}>

// 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

📄 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:

  • @uoj-lk/auth-node - Node.js/Express backend middleware (coming soon)

Made with ❤️ by University of Jaffna