Authentication & Authorization
Admin app authentication flows using OAuth with Microsoft/Azure, and role-based access control.
Authentication & Authorization
The admin app uses OAuth with Microsoft/Azure for authentication and role-based access control (RBAC) to restrict access to admin features.
The admin app uses OAuth with Microsoft/Azure (BISO accounts) for authentication. There is NO email/password login for administrators.
Roles & Permissions
Role Hierarchy
- Admin - Full system access, can manage everything
- Editor - Content management, cannot manage users or settings
- Viewer - Read-only access to content
Role Implementation
Roles are stored as labels in Appwrite user accounts:
// Check if user has admin role
const user = await account.get();
const isAdmin = user.labels?.includes('admin');
const isEditor = user.labels?.includes('editor');
const hasAccess = isAdmin || isEditor;Authentication Flow
Login Page
The login page offers a single authentication option - OAuth with Microsoft:
// apps/admin/src/app/(auth)/auth/login/page.tsx
import { Login } from "@/components/login";
import { getAuthStatus } from "@/lib/auth-utils";
import { redirect } from "next/navigation";
export default async function Page({ searchParams }: {
searchParams: Promise<{ redirectTo?: string, error?: string }>
}) {
const authStatus = await getAuthStatus();
const { error, redirectTo } = await searchParams;
if (authStatus.isAuthenticated) {
const target = redirectTo ? decodeURIComponent(redirectTo) : '/admin';
return redirect(target);
}
return <Login />;
}OAuth Implementation
Initiate OAuth Flow
Server Action (apps/admin/src/lib/server.ts):
'use server';
import { createAdminClient } from '@repo/api/server';
import { headers } from 'next/headers';
import { OAuthProvider } from '@repo/api';
import { redirect } from 'next/navigation';
export async function signInWithAzure() {
const { account } = await createAdminClient();
const origin = (await headers()).get("origin");
// Get redirect parameter from URL if exists
const url = new URL((await headers()).get("referer") || `${origin}/auth/login`);
const redirectTo = url.searchParams.get("redirectTo");
// Include redirectTo in success URL
const successUrl = redirectTo ?
`${origin}/auth/oauth?redirectTo=${redirectTo}` :
`${origin}/auth/oauth`;
const redirectUrl = await account.createOAuth2Token(
OAuthProvider.Microsoft,
successUrl,
`${origin}/auth/login`
);
return redirect(redirectUrl);
}Handle OAuth Callback
OAuth Callback Route (apps/admin/src/app/(auth)/auth/oauth/route.ts):
import { createAdminClient } from "@repo/api/server";
import { cookies } from "next/headers";
import { NextRequest } from "next/server";
import { redirect } from "next/navigation";
export async function GET(request: NextRequest) {
const userId = request.nextUrl.searchParams.get("userId");
const secret = request.nextUrl.searchParams.get("secret");
const redirectTo = request.nextUrl.searchParams.get("redirectTo");
const { account } = await createAdminClient();
const session = await account.createSession(userId, secret);
(await cookies()).set("a_session_biso", session.secret, {
path: "/",
httpOnly: true,
sameSite: "none",
secure: true,
domain: ".biso.no"
});
// Redirect to original destination or admin dashboard
if (redirectTo) {
return redirect(decodeURIComponent(redirectTo));
}
return redirect(`/admin`);
}Middleware Protection
// middleware.ts
import { createSessionClient } from '@repo/api/server';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Protect admin routes
if (pathname.startsWith('/admin')) {
try {
const { account } = await createSessionClient();
const user = await account.get();
// Verify user has required role
const hasAccess = user.labels?.includes('admin') ||
user.labels?.includes('editor');
if (!hasAccess) {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
} catch {
return NextResponse.redirect(new URL('/auth/login', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/admin/:path*'],
};Permission Checks in Components
// components/admin/delete-button.tsx
'use client';
import { useUser } from '@/hooks/use-user';
import { Button } from '@repo/ui/components/ui/button';
export function DeleteButton({ onDelete }: { onDelete: () => void }) {
const { user } = useUser();
// Only admins can delete
if (!user?.labels?.includes('admin')) {
return null;
}
return (
<Button variant="destructive" onClick={onDelete}>
Delete
</Button>
);
}Role Assignment
Admins can assign roles to users:
// app/actions/users.ts
'use server';
import { createAdminClient } from '@repo/api/server';
export async function assignRole(userId: string, role: 'admin' | 'editor' | 'viewer') {
const { account } = await createAdminClient();
// Verify current user is admin
const currentUser = await account.get();
if (!currentUser.labels?.includes('admin')) {
return {
success: false,
error: 'Unauthorized: Only admins can assign roles'
};
}
// Update user labels
await account.updateLabels(userId, [role]);
return { success: true };
}Always verify permissions on the server. Client-side checks are for UX only - never rely on them for security.
Session Management
Sessions are stored in HTTP-only cookies with domain-wide access:
cookies().set("a_session_biso", session.secret, {
path: "/",
httpOnly: true,
sameSite: "none",
secure: true,
domain: ".biso.no" // Domain-wide cookie
});Related Documentation
- User Management - Managing users and roles
- @repo/api Package - Appwrite client
- Authentication Guide - General auth patterns
