Skip to content

Commit 53e43be

Browse files
docs: expand authentication guide with implementation examples and RBAC details
1 parent 4884074 commit 53e43be

1 file changed

Lines changed: 173 additions & 26 deletions

File tree

apps/docs/docs/main-components/frontend-app/authentication.md

Lines changed: 173 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -306,34 +306,117 @@ export const { GET, POST } = handlers;
306306

307307
## Implementation Guidelines
308308

309-
### Securing Routes
309+
### Securing routes
310310

311-
Use the `auth()` function to protect routes:
311+
The application uses different strategies to protect routes depending on the component type (Server vs. Client) and the specific requirements of the page.
312312

313-
```typescript
314-
import { auth } from '@/auth';
313+
#### Server components
314+
315+
In Server Components, use the `auth()` function to retrieve the current session. If the session is missing, you can redirect the user to the login page.
316+
317+
```typescript title="protecting a Server Component"
318+
import { auth, signIn } from '@/auth';
315319

316320
export default async function ProtectedPage() {
317321
const session = await auth();
318322

319323
if (!session) {
320-
redirect('/login');
324+
// Option 1: Trigger sign-in flow
325+
return await signIn();
326+
327+
// Option 2: Redirect to login page
328+
// redirect('/login');
321329
}
322330

323331
// Render protected content
324332
}
325333
```
326334

327-
### Role-Based Access Control
335+
#### Handling authentication errors
336+
337+
When making API calls via the SDK, you should handle authentication errors (like `401 Unauthorized` or token expiration). If the SDK returns a `401`, it usually means the session has expired or the token is invalid.
338+
339+
```typescript title="handling 401 and token errors"
340+
try {
341+
const data = await sdk.modules.getPage({ slug }, {}, session?.accessToken);
342+
} catch (error) {
343+
if (error?.status === 401) {
344+
if (!session?.user) {
345+
// User is not logged in, trigger sign-in
346+
return await signIn();
347+
} else {
348+
// User is logged in but unauthorized for this page
349+
notFound();
350+
}
351+
}
352+
}
353+
```
328354

329-
Check user roles to control access to features:
355+
Additionally, if the session contains a `RefreshTokenError`, you should force the user to re-authenticate:
356+
357+
```typescript
358+
if (session?.error === 'RefreshTokenError') {
359+
return await signIn();
360+
}
361+
```
362+
363+
#### Client components
364+
365+
In Client Components, use the `useSession` hook. Note that `useSession` provides a `status` field (`loading`, `authenticated`, `unauthenticated`) which you can use to protect the UI.
366+
367+
```typescript title="protecting a Client Component"
368+
'use client';
369+
import { useSession } from 'next-auth/react';
370+
371+
export default function ClientPage() {
372+
const { data: session, status } = useSession();
373+
374+
if (status === 'loading') return <Spinner />;
375+
if (status === 'unauthenticated') return <RedirectToLogin />;
376+
377+
return <div>Welcome, {session.user.name}</div>;
378+
}
379+
```
380+
381+
### Role-based access control
382+
383+
O2S supports role-based access control (RBAC) at both the user level and the organization (customer) level.
384+
385+
#### User-level roles
386+
387+
The `session` object includes a top-level `role` for the user, which typically represents their global system role.
330388

331389
```typescript
332390
if (session?.user?.role === 'selfservice_admin') {
333-
// Show admin features
391+
// Show global admin features
392+
}
393+
```
394+
395+
#### Organization-level roles
396+
397+
In B2B scenarios, a user might have different roles depending on the current customer context. These are stored within the `customer` object in the session.
398+
399+
```typescript
400+
const orgRoles = session?.user?.customer?.roles || [];
401+
402+
if (orgRoles.includes('organization_manager')) {
403+
// Show features specific to organization managers
404+
}
405+
```
406+
407+
#### Granular permissions
408+
409+
While roles provide a broad check, O2S encourages using **permission-based access control** for specific features and blocks. Permissions are typically part of the data returned by the SDK for a specific block or module.
410+
411+
```typescript
412+
// Example: Checking a specific permission within a block
413+
if (blockData.permissions?.canEdit) {
414+
return <EditButton />;
334415
}
335416
```
336417

418+
For more details on how the API Harmonization server determines these permissions based on the user's roles, see the [API Harmonization authentication documentation](../harmonization-app/authentication.md).
419+
337420
### Permission-based access control in blocks
338421

339422
Blocks can include permission flags in their responses that indicate what actions the user can perform. The frontend uses these flags to conditionally render features.
@@ -401,34 +484,98 @@ This pattern allows you to:
401484

402485
The permission flags come from the API Harmonization server, which checks the user's permissions from their JWT token. For more details on how permissions are checked and enforced, see the [API Harmonization authentication documentation](../harmonization-app/authentication.md).
403486

404-
### Customer Context Switching
487+
### Customer context switching
405488

406-
To implement customer switching:
489+
For B2B scenarios, users can be associated with multiple customer accounts (organizations) and switch between them. To implement this in the frontend, you should use the `updateOrganization` facade.
407490

408-
```typescript
409-
// Update session with new customer context
410-
await update({
411-
customerId: selectedCustomerId,
412-
});
491+
Instead of calling session updates directly in your components, O2S provides a facade in `src/auth/auth.organizations.ts`. This allows the UI to remain decoupled from the specific IAM implementation being used.
492+
493+
```typescript title="triggering context switch (client-side)"
494+
import { updateOrganization } from '@/auth/auth.organizations';
495+
496+
const handleSwitch = async (customer: Models.Customer.Customer) => {
497+
// This call triggers the integration-specific update logic
498+
await updateOrganization(session, customer);
499+
};
413500
```
414501

415-
## Extending Authentication
502+
The `updateOrganization` utility is a wrapper around the current IAM integration's implementation. For example, in the mocked integration, it is located at `packages/integrations/mocked/src/auth/auth.updateOrganization.ts`.
503+
504+
A typical implementation follows these steps:
505+
506+
1. Update user context by making an API call to your IAM or backend to update the user's active organization.
507+
2. Use `session.update()` to refresh the NextAuth session with the new roles and permissions.
508+
3. Refresh app state which usually involves a `window.location.reload()` or a redirect to ensure all hooks and SDK instances are re-initialized with the new context.
416509

417-
### Adding New Providers
510+
```typescript title="example implementation (from mocked integration)"
511+
export async function updateOrganization(session: ReturnType<typeof useSession>, customer: Models.Customer.Customer) {
512+
// 1. Update session with new customer data
513+
await session.update({
514+
customer,
515+
});
418516

419-
To add a new authentication provider:
517+
// 2. Refresh to apply changes across the application
518+
window.location.reload();
519+
}
520+
```
521+
522+
In a production environment with a real IAM system, this function might first perform an asynchronous request to exchange the current token for one scoped to the selected organization before updating the session.
523+
524+
The following example is inspired by the `ContextSwitcher` component, showing how to handle the organization selection:
525+
526+
```typescript title="ContextSwitcher selection logic"
527+
import { updateOrganization } from '@/auth/auth.organizations';
528+
529+
const onSubmit = async (values: ContextSwitcherFormValues) => {
530+
spinner.toggle(true);
531+
532+
try {
533+
const customer = data.items.find((item) => item.id === values.customer);
534+
if (!customer) {
535+
throw new Error('No customer found');
536+
}
420537

421-
1. Install the required package
422-
2. Add provider configuration to `auth.providers.ts`
423-
3. Update UI to include the new sign-in option
538+
// Use the facade to handle the update
539+
await updateOrganization(session, customer);
540+
} catch (error) {
541+
console.error('Failed to update organization:', error);
542+
} finally {
543+
spinner.toggle(false);
544+
}
545+
};
546+
```
547+
548+
### Adding new providers
549+
550+
O2S supports any authentication provider compatible with [Auth.js](https://authjs.dev/reference/core/providers).
551+
552+
Standard OAuth providers (like GitHub, Google, Azure AD) or the Credentials provider are included in the `next-auth` package. You can then add the new provider to the `providers` array in `apps/frontend/src/auth/auth.providers.ts`. The application automatically processes this array to generate the login UI.
553+
554+
```typescript title="apps/frontend/src/auth/auth.providers.ts"
555+
import Google from 'next-auth/providers/google';
556+
557+
export const providers: Provider[] = [
558+
// ...existing providers
559+
Google({
560+
clientId: process.env.GOOGLE_CLIENT_ID,
561+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
562+
profile(profile) {
563+
return {
564+
id: profile.sub,
565+
email: profile.email,
566+
name: profile.name,
567+
role: process.env.AUTH_DEFAULT_USER_ROLE, // Assign default O2S role
568+
};
569+
},
570+
}),
571+
];
572+
```
424573

425-
### Custom User Data
574+
The login page uses the `providerMap` (defined in the same file) to dynamically render sign-in buttons for all configured OAuth providers. Once added to the `providers` array, the new option will automatically appear on the `/login` page (unless it is a `credentials` provider which is handled separately).
426575

427-
To store additional user data:
576+
#### Delegating the login page
428577

429-
1. Extend the Prisma User model
430-
2. Update the JWT and Session type definitions
431-
3. Modify the JWT callback to include the additional data
578+
It is also possible to completely delegate the login page to your IAM system (e.g., use login pages provided directly by Keycloak or Azure AD). To do this, simply configure `auth.providers.ts` to **NOT** include the `Credentials` provider. When no `Credentials` provider is present, you can configure your application to redirect users directly to the IAM's login page instead of using the O2S built-in login screen.
432579

433580
## References
434581

0 commit comments

Comments
 (0)