Skip to content

Portal Authentication

The borrower portal uses a custom AuthContext for session-based authentication, separate from the admin dashboard's react-admin auth provider.

AuthContext

The AuthProvider component manages authentication state:

interface AuthContextValue {
  borrower: BorrowerProfile | null;
  isAuthenticated: boolean;   // derived from borrower !== null
  isLoading: boolean;         // initial session check in progress
  login(email: string, password: string): Promise<void>;
  logout(): Promise<void>;
}

Session Check on Mount

When the portal loads, the AuthProvider checks for an existing session:

  1. Calls getMe() to fetch the borrower profile from GET /api/v1/portal/me
  2. If successful, sets the borrower in state (user is already logged in)
  3. If it fails (401), leaves borrower as null
  4. Sets isLoading = false after the check completes

This enables "remember me" behavior --- if the session cookie is still valid, the user is automatically logged in.

Login Flow

1. ensureCsrf()                 → GET /api/v1/auth/session (establish CSRF cookie)
2. authApi.login(email, pass)   → POST /api/v1/auth/login + X-CSRFToken
3. authApi.getMe()              → GET /api/v1/portal/me
4. Set borrower in state

Logout Flow

1. authApi.logout()             → DELETE /api/v1/auth/session + X-CSRFToken
2. Clear borrower from state
3. ProtectedRoute redirects to /login

useAuth() Hook

Components access auth state via the useAuth() hook:

function DashboardPage() {
  const { borrower, logout } = useAuth();
  return <h1>Welcome, {borrower?.first_name}</h1>;
}

Throws an error if used outside <AuthProvider>.

ProtectedRoute

The <ProtectedRoute> component guards all portal routes:

isLoading = true  → <LoadingScreen> (spinner)
isAuthenticated = false  → Navigate to /login
isAuthenticated = true   → <Outlet> (render child route)

This ensures:

  • No flash of content before auth check completes
  • Unauthenticated users are always redirected to login
  • The redirect preserves the intended URL for post-login navigation

Backend Permission

The backend uses the IsBorrowerUser permission class to restrict portal endpoints:

  • Checks that the authenticated user has a portal_user mapping to a borrower
  • All portal API data is scoped to the authenticated borrower's records
  • A borrower can only see their own loans, payments, documents, etc.

Borrower-to-User Mapping

The Borrower model has a portal_user FK to the User model:

Borrower.portal_user → User (optional)

When a user authenticates via the portal, the backend looks up the borrower linked to that user. If no mapping exists, the user cannot access portal endpoints.

Password Reset

The portal includes forgot/reset password pages that use the same backend endpoints as the admin dashboard:

Forgot Password (/forgot-password):

  • Collects email address
  • POST /api/v1/auth/password/request
  • Always shows success (prevents email enumeration)

Reset Password (/reset-password?token=...):

  • Validates token from query parameter
  • Collects new password + confirmation
  • POST /api/v1/auth/password/reset with token and new password

See Also