npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@uoj-lk/auth-react

v3.4.0

Published

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

Readme

@uoj-lk/auth-react

React authentication toolkit for OAuth 2.0 + PKCE, httpOnly cookies, roles, and permissions.

npm version License: MIT

Docs: Index · OAuth Quick Start · OAuth Guide

Purpose

  • Drop-in React provider, components, and hooks for the UoJ Auth Service
  • Secure public-client OAuth with PKCE; session cookies for API calls
  • Permission/role helpers for rendering and guarding routes

Installation

npm install @uoj-lk/auth-react
npm install react react-dom react-router-dom

Minimal Setup (SPA, OAuth + PKCE)

import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import {
  AuthProvider,
  OAuthButton,
  OAuthCallback,
  ProtectedRoute,
  AuthErrorBoundary,
} from "@uoj-lk/auth-react";
import Dashboard from "./Dashboard";

const authConfig = {
  apiUrl: "https://api.example.com/api",
  clientId: "your-client-id",
  requestTimeout: 10000,
  enableDevLogging: process.env.NODE_ENV === "development",
};

export default function App() {
  return (
    <BrowserRouter>
      <AuthErrorBoundary>
        <AuthProvider config={authConfig}>
          <Routes>
            <Route
              path="/login"
              element={
                <OAuthButton authUrl="https://auth.example.com/oauth/authorize">
                  Sign in with OAuth
                </OAuthButton>
              }
            />
            <Route
              path="/oauth/callback"
              element={
                <OAuthCallback tokenUrl="https://api.example.com/api/oauth/token" />
              }
            />
            <Route
              path="/dashboard"
              element={
                <ProtectedRoute>
                  <Dashboard />
                </ProtectedRoute>
              }
            />
          </Routes>
        </AuthProvider>
      </AuthErrorBoundary>
    </BrowserRouter>
  );
}

Use auth state

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

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

  return (
    <div>
      <h1>Hello {user?.firstName}</h1>
      <button onClick={logout}>Logout</button>
      {hasRole("admin") && <p>Admin tools</p>}
      {hasPermission("course", "create") && <button>Create course</button>}
    </div>
  );
}

Optional: React Query caching

Wrap your app in QueryClientProvider to cache /me and /permissions for 5 minutes automatically.

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

<QueryClientProvider client={queryClient}>
  {/* AuthProvider + routes here */}
</QueryClientProvider>;

Error helpers

import {
  isNetworkError,
  isTimeoutError,
  getNetworkErrorMessage,
} from "@uoj-lk/auth-react";

async function saveProfile() {
  try {
    // call your API
  } catch (error) {
    if (isTimeoutError(error)) return alert("Request took too long");
    if (isNetworkError(error)) return alert("You appear offline");
    return alert(getNetworkErrorMessage(error));
  }
}

Best practices

  • Always wrap routes with AuthProvider; add AuthErrorBoundary to isolate auth failures
  • For SPAs, omit clientSecret and use PKCE (default)
  • Set requestTimeout to avoid hanging requests; enable enableDevLogging only in development
  • Use ProtectedRoute for gating; use hasRole/hasPermission inside components
  • For fewer network calls, wrap the app in QueryClientProvider (TanStack React Query v5)

More docs

See CHANGELOG for release notes. function App() { return ( <Route path="/login" element={} /> <Route 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 <form onSubmit={handleSubmit}>...</form>;
}

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

🔓 OAuth 2.0 Components (v3.0.0)

OAuthButton

Initiates OAuth 2.0 Authorization Code + PKCE flow.

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

<OAuthButton
  authUrl="https://auth.example.com/oauth/authorize"
  scope="openid profile email"
  callbackPath="/oauth/callback"
  className="btn btn-primary"
>
  Sign in with OAuth
</OAuthButton>;

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 |

OAuthCallback

Handles OAuth callback, validates state, exchanges code for tokens.

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

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

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

RequireScope / RequirePermissionInScope / RequireAnyPermissionInScope (v3.3.0)

The React counterparts of @uoj-lk/auth-node's requirePermissionInScope and requireAnyPermissionInScope. Scopes (e.g. ENROLLMENT) are attached to a user-role assignment, so a permission only counts inside a scope when the same assignment grants both — a permission from one role and a scope from another never combine into access. Use these wherever the API route behind the UI is scope-guarded, so the button and the endpoint agree.

import {
  RequireScope,
  RequirePermissionInScope,
  RequireAnyPermissionInScope,
} from "@uoj-lk/auth-react";

// Any role assignment carrying the scope
<RequireScope scope="ENROLLMENT">
  <EnrollmentTools />
</RequireScope>

// One permission, inside the scope
<RequirePermissionInScope scope="ENROLLMENT" permission="enrollments:approve">
  <ApproveButton />
</RequirePermissionInScope>

// ALL of these permissions, inside the scope
<RequirePermissionInScope
  scope="ENROLLMENT"
  permissions={["enrollments:read", "enrollments:update"]}
>
  <EnrollmentEditor />
</RequirePermissionInScope>

// ANY of these permissions, inside the scope
<RequireAnyPermissionInScope
  scope="ENROLLMENT"
  permissions={["enrollments:read", "enrollments:list"]}
>
  <EnrollmentList />
</RequireAnyPermissionInScope>

RequireAnyScopedPermission (v3.4.0)

The gates above hold the scope fixed and vary the permission. When the same content is reached by two different roles through two different scopes, pass the [scope, permission] pairs instead — any one of them is enough, and each pair is still matched within a single role assignment. This mirrors auth-node's requireAnyScopedPermission, so the gate agrees with the route:

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

// Admissions staff reach this through ENROLLMENT,
// the registration desk through ACADEMIC_REGISTRATION
<RequireAnyScopedPermission
  alternatives={[
    ["ENROLLMENT", "student-count:read"],
    ["ACADEMIC_REGISTRATION", "students:read"],
  ]}
>
  <StudentDemographicChart />
</RequireAnyScopedPermission>;

The same check is available as hasAnyScopedPermission(alternatives) on useAuth(), usePermissionCheck() and useScopedAccess() (and as canAccessAnyScope on usePermissionCheck()).

All permission gates also take loadingFallback, rendered while /auth/permissions is still in flight so the user never sees a denial for a question that has not been answered yet:

<RequirePermissionInScope
  scope="ENROLLMENT"
  permission="enrollments:approve"
  loadingFallback={<Spinner />}
  fallback={<ReadOnlyBadge />}
>
  <ApproveButton />
</RequirePermissionInScope>

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

  // Scoped Permission Checks (NEW v3.3) — mirrors auth-node
  hasScope, // Holds a scope through any role
  hasPermissionInScope, // Permission + scope from the SAME role assignment
  hasAnyPermissionInScope, // Any of the permissions, within the scope
  hasAllPermissionsInScope, // All of the permissions, within the scope
  getScopes, // All scope names the user holds
  getPermissionsInScope, // All permissions held inside one scope

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

Examples

OAuth Login (v3.0.0)

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

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

🚨 Error Handling (v3.1.0)

useErrorHandler Hook

Handle API errors with permission-aware messaging:

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

usePermissionCheck Hook

Check permissions with fallback support:

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
}

useScopedAccess Hook (v3.3.0)

Scope-aware checks bundled with the user's scopes and one loading flag, so a component can tell "denied" apart from "not loaded yet":

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

const { scopes, hasScope, hasAnyPermissionInScope, isLoading } =
  useScopedAccess();

const canApprove = hasAnyPermissionInScope(["enrollments:approve"], "ENROLLMENT");

if (isLoading) return <Spinner />;
return <button disabled={!canApprove}>Approve</button>;

usePermissionCheck() also gains canAccessInScope(permissions, scope) plus the four scoped helpers.

useAuthState Hook

Simple access to authentication state:

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

const { isReady, isLoading, isPermissionsLoading, isAuthenticated } =
  useAuthState();

if (!isReady) {
  return <Loading />; // Wait for both auth and permissions
}

// Safe to check permissions now

PermissionErrorDisplay Component

Display formatted permission errors:

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

const [error, setError] = useState(null);

<PermissionErrorDisplay
  error={error}
  onDismiss={() => setError(null)}
  dismissable={true}
/>;

AsyncActionWithErrorHandling Component

Handle async actions with automatic error display:

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

<AsyncActionWithErrorHandling
  onAction={async () => await api.deleteRole(roleId)}
  actionLabel="Delete Role"
  onSuccess={() => toast.success("Role deleted")}
/>;

FormWithPermissionHandling Component

Forms with permission-aware error handling:

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

<FormWithPermissionHandling
  onSubmit={handleSubmit}
  successMessage="Role created successfully"
>
  <MyFormFields />
</FormWithPermissionHandling>;

�️ OAuth Utilities (v3.0.0)

For advanced OAuth use cases, you can use the low-level utilities directly:

PKCE Utilities

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();

OAuth Flow Utilities

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

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

OAuth 2.0 Security (v3.0.0)

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.

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

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

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

OAuth Issues (v3.0.0)

"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.

General Issues

"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

� Migration Guide

From v2.x to v3.0.0

Good news: No breaking changes! All v2.x code continues to work.

What's New

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

    npm install @uoj-lk/[email protected]
  2. Add OAuth routes (your existing session login still works)

    import { OAuthButton, OAuthCallback } from "@uoj-lk/auth-react";
    
    <Routes>
      {/* Existing session login - still works */}
      <Route path="/login" element={<LoginPage />} />
    
      {/* New OAuth option - add alongside */}
      <Route
        path="/oauth/start"
        element={<OAuthButton authUrl="...">OAuth Login</OAuthButton>}
      />
      <Route path="/oauth/callback" element={<OAuthCallback tokenUrl="..." />} />
    </Routes>;
  3. Update config (optional - remove clientSecret for public clients)

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