This document explains how authentication and authorization work in the Cloud Portal.
The Cloud Portal uses OpenID Connect (OIDC) for authentication and Role-Based Access Control (RBAC) for authorization.
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β Browser ββββββΊβ Cloud PortalββββββΊβ OIDC β
β βββββββ (Hono) βββββββ Provider β
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β
βΌ
βββββββββββββββ
β Control β
β Plane API β
βββββββββββββββ
User clicks "Login" β redirected to OIDC provider:
/login β OIDC Provider β /auth/callback
After successful authentication:
- OIDC provider redirects to
/auth/callbackwith authorization code - Server exchanges code for tokens (access token, refresh token, ID token)
- Session created with tokens
- User redirected to dashboard
Sessions are managed server-side:
// Session structure
interface Session {
accessToken: string;
refreshToken: string;
idToken: string;
expiresAt: number;
user: {
id: string;
email: string;
name: string;
};
}# Required
AUTH_OIDC_ISSUER=https://your-oidc-provider.com
AUTH_OIDC_CLIENT_ID=your-client-id
# Required (server-side only)
SESSION_SECRET=minimum-32-character-secret-keyYour OIDC provider must have:
- Redirect URI:
http://localhost:3000/auth/callback(dev) - Redirect URI:
https://cloud.datum.net/auth/callback(prod) - Scopes:
openid profile email
The session middleware validates authentication on each request:
// app/server/middleware/auth.ts
export function sessionMiddleware() {
return async (c: Context, next: Next) => {
const session = await getSession(c);
if (session && isSessionValid(session)) {
c.set('session', session);
}
await next();
};
}export async function loader({ context }: Route.LoaderArgs) {
const { session } = context;
// Redirect if not authenticated
if (!session) {
throw redirect('/login');
}
// Use access token for API calls
const data = await fetchWithToken(session.accessToken);
return { data, user: session.user };
}| Property | Type | Description |
|---|---|---|
session |
Session | null |
Current user session |
session.accessToken |
string |
JWT for API calls |
session.user |
User |
User info (id, email, name) |
- Roles are defined in the Control Plane
- Users are assigned roles (directly or via groups)
- Portal checks permissions before showing features
Use the access-review resource to check permissions:
import { checkAccess } from '@/resources/access-review';
// Check if user can create DNS zones
const canCreate = await checkAccess({
action: 'create',
resource: 'dns-zones',
scope: { projectId: currentProject.id },
});
if (!canCreate) {
// Hide or disable the feature
}Organization Owner
β
βΌ
Organization Admin
β
βΌ
Project Owner
β
βΌ
Project Admin
β
βΌ
Project Member (Viewer)
// In loader
export async function loader({ context }: Route.LoaderArgs) {
const { session } = context;
if (!session) {
// Redirect to login with return URL
const url = new URL(request.url);
throw redirect(`/login?returnTo=${url.pathname}`);
}
return { user: session.user };
}function AdminPanel() {
const { user, permissions } = useLoaderData();
if (!permissions.includes('admin')) {
return <AccessDenied />;
}
return <AdminContent />;
}Tokens are automatically refreshed when they expire:
// Handled by auth middleware
if (isTokenExpired(session.accessToken)) {
const newTokens = await refreshTokens(session.refreshToken);
await updateSession(newTokens);
}Tokens are automatically injected into API calls via AsyncLocalStorage:
// In server/middleware/request-context.ts
export function requestContextMiddleware() {
return async (c: Context, next: Next) => {
const session = c.get('session');
// Store token in AsyncLocalStorage
await runWithContext(
{
token: session?.accessToken,
requestId: c.get('requestId'),
},
next
);
};
}This allows generated API clients to automatically include auth:
// This automatically includes the token
const orgs = await getOrganizations();- User clicks "Logout"
- Session destroyed on server
- Redirect to OIDC provider's logout endpoint
- Redirect back to login page
// Logout route
app.get('/logout', async (c) => {
await destroySession(c);
const logoutUrl = new URL('/logout', AUTH_OIDC_ISSUER);
logoutUrl.searchParams.set('post_logout_redirect_uri', APP_URL);
return c.redirect(logoutUrl.toString());
});- Sessions stored server-side only
SESSION_SECRETmust be 32+ characters- Secure cookies in production (HTTPS only)
- HttpOnly cookies (no JS access)
- Access tokens never exposed to client JS
- Refresh tokens stored server-side only
- Short token expiration (15 min)
- Automatic refresh before expiry
- SameSite cookies enabled
- CSRF tokens for mutations
- Origin validation
- Check session exists:
context.session - Verify token not expired
- Check API_URL is correct
- Verify OIDC configuration
- Check redirect URI in OIDC provider
- Verify
AUTH_OIDC_ISSUERis correct - Check browser cookies are enabled
- Check refresh token not expired
- Verify OIDC provider is accessible
- Check
SESSION_SECREThasn't changed
- Environment Setup - Auth configuration
- ADR-005: Unified Environment Config