Authentication System
ABP React implements a comprehensive authentication system using OpenID Connect with PKCE (Proof Key for Code Exchange) flow. This guide explains how the authentication works, from login to session management.
Overview
The authentication system in ABP React is built around:
- OpenID Connect (OIDC) with PKCE flow for secure authentication
- Iron Session for server-side session management
- Redis for distributed session storage
- JWT tokens for API authorization
- Multi-tenancy support with tenant-aware authentication
Authentication Flow
1. Login Process
The login process follows the OpenID Connect authorization code flow with PKCE:
Step 1: User Initiates Login
When a user clicks the login button, they are redirected to /auth/login
:
// src/components/Login.tsx
const Login = () => {
const handleLogin = () => {
window.location.href = '/auth/login'
}
// ...
}
Step 2: Authorization Request
The /auth/login
route handler generates PKCE parameters and redirects to ABP's authorization endpoint:
// src/app/auth/login/route.ts
export async function GET() {
const session = await getSession()
// Generate PKCE parameters
let code_verifier = client.randomPKCECodeVerifier()
let code_challenge = await client.calculatePKCECodeChallenge(code_verifier)
// Get OpenID configuration
const openIdClientConfig = await getClientConfig()
let tenantId = session.tenantId
// Build authorization URL with parameters
let parameters: Record<string, string> = {
"redirect_uri": clientConfig.redirect_uri,
"scope": clientConfig.scope!,
code_challenge,
"code_challenge_method": "S256",
"__tenant": tenantId,
}
// Store PKCE verifier in session
let redirectTo = client.buildAuthorizationUrl(openIdClientConfig, parameters)
session.code_verifier = code_verifier
session.state = state
await session.save()
return Response.redirect(redirectTo.href)
}
Step 3: User Authentication
The user is redirected to ABP's login page where they enter their credentials. After successful authentication, ABP redirects back to the configured callback URL.
Step 4: Authorization Code Exchange
The callback is handled by /auth/openiddict/route.ts
:
// src/app/auth/openiddict/route.ts
export async function GET(request: NextRequest) {
const session = await getSession()
const openIdClientConfig = await getClientConfig()
// Exchange authorization code for tokens
const tokenSet = await client.authorizationCodeGrant(openIdClientConfig, currentUrl, {
pkceCodeVerifier: session.code_verifier,
expectedState: session.state
})
const { access_token, refresh_token } = tokenSet
// Get user information
const userinfo = await client.fetchUserInfo(openIdClientConfig, access_token, sub)
// Update session
session.isLoggedIn = true
session.access_token = access_token
session.userInfo = {
sub: userinfo.sub,
name: userinfo.given_name!,
email: userinfo.email!,
email_verified: userinfo.email_verified!,
}
await session.save()
// Store tokens in Redis for distributed access
const redisSessionData = {
access_token: access_token,
refresh_token: refresh_token,
} as RedisSession
const redis = createRedisInstance()
const redisKey = `session:${session.userInfo.sub}`
await redis.set(redisKey, JSON.stringify(redisSessionData))
await redis.quit()
return Response.redirect(clientConfig.post_login_route)
}
2. Session Management
Iron Session Configuration
Sessions are managed using Iron Session with secure configuration:
// src/sessionOptions.ts
export const sessionOptions: SessionOptions = {
password: process.env.SESSION_PASSWORD || 'complex_password_at_least_32_characters_long',
cookieName: 'abp-react-session-id',
cookieOptions: {
secure: process.env.NODE_ENV === 'production',
},
ttl: 60 * 60 * 24 * 7, // 1 week
}
Session Data Structure
The session contains comprehensive user and authentication data:
// src/lib/session-utils.ts
export interface SessionData {
isLoggedIn: boolean
access_token?: string
code_verifier?: string
state?: string
userInfo?: {
sub: string
name: string
email: string
email_verified: boolean
}
tenantId?: string
}
Client-Side Session Access
Components use the useSession
hook to access session data:
// src/useSession.ts
export default function useSession(): UseQueryResult<SessionData, Error> {
return useQuery({
queryKey: [QueryNames.GetSession],
queryFn: async () => {
const response = await fetch('/session')
return (await response.json()) as SessionData
},
refetchOnWindowFocus: true
})
}
3. Token Management
Automatic Token Refresh
The system automatically refreshes expired tokens:
// src/lib/actions.ts
export async function getSession(): Promise<IronSession<SessionData>> {
let session = await getIronSession<SessionData>(await cookies(), sessionOptions)
try {
// Check if the access token is expired
if (session.access_token && isTokenExpired(session.access_token!)) {
const redisKey = `session:${session?.userInfo?.sub!}`
const redis = createRedisInstance()
const clientConfig = await getClientConfig()
// Retrieve session data from Redis
let redisSessionData = await redis.get(redisKey)
const parsedSessionData = JSON.parse(redisSessionData!) as RedisSession
// Refresh the access token using the refresh token
const tokenSet = await client.refreshTokenGrant(clientConfig, parsedSessionData.refresh_token!)
session.access_token = tokenSet.access_token
await session.save()
// Update Redis with the new session data
const newRedisSessionData = {
access_token: tokenSet.access_token,
refresh_token: tokenSet.refresh_token,
} as RedisSession
await redis.set(redisKey, JSON.stringify(newRedisSessionData))
await redis.quit()
}
return session
} catch (error) {
console.error('Error getting session:', error)
// Return default session on error
return session
}
}
Token Expiration Check
// src/lib/auth.ts
export const isTokenExpired = (token: string) => {
const decoded = jwtDecode(token!);
const expirationTime = decoded?.exp! * 1000;
const currentTime = new Date().getTime();
return expirationTime < currentTime
}
Multi-Tenancy Integration
Tenant Selection
Before authentication, users must select their tenant:
// src/app/auth/set-tenant/route.ts
export async function GET() {
await setUpLayoutConfig()
const session = await getSession()
const host = (await headers()).get('host')
if (session.tenantId) {
return
}
try {
const { data } = await tenantGetTenantGuid({ query: { host: host! } })
session.tenantId = data ?? 'default'
} catch (error) {
console.error('Failed to fetch tenant GUID:', error)
session.tenantId = 'default'
}
await session.save()
redirect('/')
}
Middleware Protection
The middleware ensures users have a tenant selected:
// src/middleware.ts
export async function middleware(request: NextRequest): Promise<NextResponse | undefined> {
const session = await getIronSession<SessionData>(await cookies(), sessionOptions)
// Check if tenantId is present in the session
if (!session.tenantId && request.nextUrl.pathname !== '/auth/set-tenant') {
let redirectUrl = new URL('/auth/set-tenant', request.nextUrl.origin)
return NextResponse.redirect(redirectUrl)
}
}
API Integration
Automatic Token Injection
API requests automatically include authentication headers:
// src/lib/auth.ts
export const setUpLayoutConfig = async () => {
APIClient.setConfig({
baseUrl: typeof window !== 'undefined' ? '' : process.env.NEXT_PUBLIC_API_URL!,
})
APIClient.interceptors.request.use(async (options) => {
const session = await getSession()
options.headers.set('Authorization', `Bearer ${session.access_token}`)
options.headers.set('__tenant', session.tenantId ?? '')
return options
})
}
Proxy API Routes
API calls are proxied through Next.js routes to include authentication:
// src/app/api/[...slug]/route.ts
export async function handler(request: NextRequest) {
const session = await getSession()
// Add authentication headers
headers.set('Authorization', `Bearer ${session.access_token}`)
headers.set('__tenant', session.tenantId ?? '')
// Forward request to ABP backend
const response = await fetch(targetUrl, {
method: request.method,
headers,
body: request.body,
})
return response
}
Logout Process
The logout process cleans up all session data:
// src/app/auth/logout/route.ts
export async function GET() {
const session = await getSession()
const redis = createRedisInstance()
const redisKey = `session:${session.userInfo?.sub}`
// Get session data from Redis
const redisSessionData = await redis.get(redisKey);
const parsedSessionData = JSON.parse(redisSessionData!) as RedisSession;
// Build end session URL
const openIdClientConfig = await getClientConfig()
const endSessionUrl = client.buildEndSessionUrl(openIdClientConfig, {
post_logout_redirect_uri: clientConfig.post_logout_redirect_uri,
id_token_hint: parsedSessionData.access_token,
})
// Clear session data
session.isLoggedIn = defaultSession.isLoggedIn
session.access_token = defaultSession.access_token
session.userInfo = defaultSession.userInfo
// Remove from Redis
await redis.del(session?.userInfo?.sub!)
await session.save()
return Response.redirect(endSessionUrl.href)
}
Configuration
Environment Variables
Configure authentication settings in your environment:
# Application URLs
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_API_URL=http://localhost:44300
# OpenID Connect Configuration
NEXT_PUBLIC_CLIENT_ID=your-client-id
NEXT_PUBLIC_SCOPE=openid profile email phone roles
# Session Security
SESSION_PASSWORD=your-secure-session-password-at-least-32-characters
# Redis Configuration (for distributed sessions)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=your-redis-password
Client Configuration
The OpenID client is configured in src/config.ts
:
export const clientConfig = {
url: process.env.NEXT_PUBLIC_API_URL,
audience: process.env.NEXT_PUBLIC_API_URL,
client_id: process.env.NEXT_PUBLIC_CLIENT_ID,
scope: process.env.NEXT_PUBLIC_SCOPE,
redirect_uri: `${process.env.NEXT_PUBLIC_APP_URL}/auth/openiddict`,
post_logout_redirect_uri: `${process.env.NEXT_PUBLIC_APP_URL}`,
response_type: 'code',
grant_type: 'authorization_code',
post_login_route: `${process.env.NEXT_PUBLIC_APP_URL}`,
code_challenge_method: 'S256'
}
Security Features
PKCE (Proof Key for Code Exchange)
- Prevents authorization code interception attacks
- Uses SHA256 code challenge method
- Generates secure random code verifiers
Secure Session Storage
- Iron Session with strong encryption
- HttpOnly cookies in production
- Configurable session TTL
Token Security
- JWT tokens stored securely server-side
- Automatic token refresh before expiration
- Secure token transmission via HTTPS
Multi-Tenant Security
- Tenant isolation at the authentication level
- Tenant-specific token scoping
- Secure tenant selection process
Troubleshooting
Common Issues
1. Authentication Redirect Loop
Cause: Session configuration issues or tenant selection problems.
Solution:
- Check that
SESSION_PASSWORD
is set correctly - Verify tenant configuration
- Clear browser cookies and try again
2. Token Refresh Failures
Cause: Redis connection issues or expired refresh tokens.
Solution:
- Check Redis connection configuration
- Verify refresh token is being stored correctly
- Check OpenID Connect configuration
3. API Authorization Errors
Cause: Token not being included in requests or expired tokens.
Solution:
- Verify
setUpLayoutConfig()
is called in layout - Check API proxy configuration
- Ensure proper error handling for token refresh
Debug Mode
Enable debug logging for authentication:
// Add to your layout or middleware
if (process.env.NODE_ENV === 'development') {
console.log('Session:', session)
console.log('Token expired:', isTokenExpired(session.access_token))
}
Best Practices
1. Secure Configuration
// Use strong session passwords
SESSION_PASSWORD=your-very-secure-password-with-at-least-32-characters
// Enable secure cookies in production
cookieOptions: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'strict'
}
2. Error Handling
const { data: session, error } = useSession()
if (error) {
// Handle authentication errors
window.location.href = '/auth/login'
}
if (!session?.isLoggedIn) {
// Show login prompt
return <LoginPrompt />
}
3. Route Protection
// Protect admin routes
export default function ProtectedPage() {
const { data: session, isLoading } = useSession()
if (isLoading) return <Loader />
if (!session?.isLoggedIn) {
redirect('/auth/login')
}
return <AdminContent />
}
Next Steps
- API Integration - Learn how to make authenticated API calls
- Multi-tenancy - Understand multi-tenant authentication
- Admin Interface - Explore the admin dashboard features
- Permissions - Set up role-based access control
The authentication system provides a robust, secure foundation for your ABP React application with enterprise-grade features like multi-tenancy, automatic token refresh, and comprehensive session management.