Authentication & Authorization
The EnterpriseAI platform uses a Backend-for-Frontend (BFF) proxy pattern for authentication. Users authenticate through Microsoft Entra ID (CIAM), Auth.js manages the session, and the BFF proxy injects bearer tokens into every platform API call server-side. Tokens are never exposed to the browser.
BFF Proxy Pattern
The BFF proxy is the core security mechanism of the platform. It sits between the browser and the platform services, ensuring that authentication tokens remain server-side at all times.
Why BFF? Storing tokens in the browser (localStorage, sessionStorage, or cookies accessible to JavaScript) exposes them to XSS attacks. The BFF pattern eliminates this risk entirely by keeping tokens in the server-side session.
How It Works
- The browser sends requests to Next.js API routes (e.g.,
/api/eai/v3/resources/...) - The BFF proxy handler reads the Auth.js session cookie
- The proxy extracts the access token from the session
- The proxy forwards the request to PublicAPI with an
Authorization: Bearer {token}header - The response flows back through the proxy to the browser
The proxy is implemented as a catch-all route at src/app/api/eai/[[...rest]]/route.ts.
Authentication Flow
Auth.js Configuration
Auth.js (NextAuth v5) is configured in src/auth.ts with the Microsoft Entra ID provider. The configuration handles:
- Provider setup -- Connects to the Entra CIAM tenant
- JWT callbacks -- Stores access tokens and refresh tokens in the session JWT
- Token refresh -- Automatically refreshes expired access tokens using the refresh token
- Session strategy -- Uses JWT strategy (no database required)
Key Auth Files
| File | Purpose |
|---|---|
src/auth.ts | Auth.js configuration with Entra ID provider, JWT callbacks, and token refresh |
src/middleware.ts | Route protection that redirects unauthenticated users to sign-in |
src/app/api/eai/[[...rest]]/route.ts | BFF proxy that injects tokens via getAccessToken() |
src/app/api/auth/[...nextauth]/route.ts | Auth.js API route handler |
Using the Session
// Server-side (Server Components, API routes)
import { auth } from '@/auth';
const session = await auth();
if (!session) {
redirect('/api/auth/signin');
}
// session.user contains: name, email, id
// Client-side (Client Components)
'use client';
import { useSession } from 'next-auth/react';
function UserProfile() {
const { data: session, status } = useSession();
if (status === 'loading') return <div>Loading...</div>;
if (status === 'unauthenticated') return <div>Please sign in</div>;
return <div>Welcome, {session.user.name}</div>;
}
Client Credentials Flow
For anonymous or service-to-service calls where no user session exists, the BFF proxy falls back to the Client Credentials flow. This uses the application's own credentials (client ID and client secret) to obtain an access token from Entra ID.
This flow is used for:
- Public-facing pages that display data without requiring sign-in
- Background jobs or cron tasks
- Server-to-server API calls
- Seeding object types during deployment
Route Protection
The src/middleware.ts file defines which routes require authentication and which are public. Protected routes redirect unauthenticated users to the sign-in page.
// src/middleware.ts
export { auth as middleware } from '@/auth';
export const config = {
matcher: [
// Protected routes (require authentication)
'/dashboard/:path*',
'/applications/:path*',
'/admin/:path*',
// Exclude public routes and static assets
'/((?!api/auth|_next/static|_next/image|favicon.ico).*)',
],
};
Environment Variables
The following environment variables configure authentication:
# Entra CIAM tenant
ENTRA_TENANT_NAME=<ciam-tenant-name> # e.g., "myorgciam"
ENTRA_TENANT_ID=<ciam-tenant-guid> # Azure AD tenant GUID
ENTRA_CLIENT_ID=<app-registration-client-id> # App registration client ID
ENTRA_CLIENT_SECRET=<app-registration-secret> # App registration client secret
ENTRA_SCOPES="email offline_access openid profile"
# Auth.js
AUTH_SECRET=<random-32-byte-base64-string> # Generate with: openssl rand -base64 32
# Platform
BASE_URL_PUBLIC_API=<publicapi-gateway-url> # PublicAPI base URL
Generate the AUTH_SECRET value with:
openssl rand -base64 32
Authorization (OPA/Authz)
After PublicAPI validates the JWT, it sends the request context to the Authz service for a policy check. Authz evaluates OPA Rego policies that define tenant-scoped RBAC rules.
Role-Based Access Control
The platform supports three standard roles, scoped per tenant:
| Role | Description |
|---|---|
tenant-user | Standard end user. Can read own resources and create new ones. |
tenant-staff | Internal staff. Can read and write all resources in the tenant. |
tenant-admin | Administrator. Full access including tenant configuration and user management. |
How Policies Are Evaluated
- PublicAPI extracts the user's tenant membership and role from the JWT claims
- PublicAPI sends the request context to Authz:
{user, tenant, action, resource_type} - Authz evaluates the Rego policy for that tenant
- Authz returns
allowordeny - If denied, PublicAPI returns
403 Forbidden
Object Type actions can specify a requiredRole to restrict who can execute them:
actions: [
{
name: 'approve',
displayName: 'Approve Application',
requiredRole: 'tenant-staff', // Only staff and above can execute
},
],