Security
App Permissions System
Operational model and UI expectations for permission-aware access control.
5.29.0
Overview
The SSO service implements a two-level access control system:
- SSO-Level Permissions: Managed by SSO admins (who can access which apps)
- App-Level Permissions: Managed by your application (what users can do inside your app)
This document focuses on SSO-level permissions that affect OAuth 2.0 authentication.
Current contract note
Canonical app-permission state comes from the permission APIs. If your app exposes a permissionStatus field internally, that should be your own backend's derived session field, not an assumed raw ID-token claim.
Permission Status (SSO-Level)
Every user has a backend-derived permissionStatus for each application they attempt to access:
App Roles (SSO-Level)
For approved users, the SSO admin assigns an app-level role:
admin- Full application access (intended for app administrators)user- Standard application access (intended for regular users)
Implementation note
📝 Note: These roles are suggestions from the SSO admin. Your application decides what features each role can access.
Deriving Permissions in Your Backend
import jwt from 'jsonwebtoken';
// WHY: ID token contains identity claims; app permissions should come
// from your backend permission lookup for the current client
const idToken = req.cookies.id_token;
const decoded = jwt.decode(idToken);
const permission = await getPermissionForUserAndClient({
userId: decoded.sub,
clientId: process.env.SSO_CLIENT_ID
});
// Extract identity fields
const {
sub: userId, // User ID
email, // User email
name, // User name
role, // Identity / broad role claim
iat, // Issued at (timestamp)
exp // Expires at (timestamp)
} = decoded;
console.log('User:', userId);
console.log('Role:', permission?.role ?? role);
console.log('Status:', permission?.status ?? 'unknown');Implementing Permission Checks
Backend Middleware (Node.js/Express)
// middleware/requireApproval.js
import jwt from 'jsonwebtoken';
export function requireApproval(req, res, next) {
const idToken = req.cookies.id_token;
if (!idToken) {
return res.status(401).json({ error: 'Not authenticated' });
}
try {
const decoded = jwt.decode(idToken);
const permission = getPermissionForUserAndClient({
userId: decoded.sub,
clientId: process.env.SSO_CLIENT_ID
});
// WHY: Check if user is approved to access this app
if (permission?.status !== 'approved') {
if (permission?.status === 'pending') {
return res.status(403).json({
error: 'APP_ACCESS_PENDING',
message: 'Your access request is pending approval'
});
}
if (permission?.status === 'revoked') {
return res.status(403).json({
error: 'APP_ACCESS_REVOKED',
message: 'Your access has been revoked'
});
}
return res.status(403).json({ error: 'APP_ACCESS_DENIED' });
}
// Attach user info to request
req.user = {
...decoded,
role: permission?.role ?? decoded.role,
permissionStatus: permission?.status ?? null
};
next();
} catch (error) {
console.error('Permission check failed:', error);
return res.status(500).json({ error: 'Permission validation failed' });
}
}
// Usage in routes
app.get('/api/protected', requireApproval, (req, res) => {
res.json({ message: 'You have access!', user: req.user });
});
// Require admin role
export function requireAdmin(req, res, next) {
requireApproval(req, res, () => {
if (req.user.role !== 'admin') {
return res.status(403).json({
error: 'INSUFFICIENT_ROLE',
message: 'Admin role required'
});
}
next();
});
}
app.delete('/api/admin/users/:id', requireAdmin, (req, res) => {
// Admin-only endpoint
});Frontend Permission Handling
// React example
import { useAuth } from './contexts/AuthContext';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
export function ProtectedRoute({ children, requireAdmin = false }) {
const { user, permission, loading } = useAuth();
const router = useRouter();
useEffect(() => {
if (loading) return;
// Not authenticated
if (!user) {
router.push('/login');
return;
}
// WHY: Check permission status
if (permission?.status === 'pending') {
router.push('/access-pending');
return;
}
if (permission?.status === 'revoked') {
router.push('/access-denied');
return;
}
if (permission?.status !== 'approved') {
router.push('/access-denied');
return;
}
// WHY: Check role requirement
if (requireAdmin && user.role !== 'admin') {
router.push('/unauthorized');
return;
}
}, [user, permission, loading, requireAdmin]);
if (loading || !user || permission?.status !== 'approved') {
return <div>Loading...</div>;
}
return <>{children}</>;
}
// Usage
function AdminDashboard() {
return (
<ProtectedRoute requireAdmin={true}>
<Title order={1} mb="xs">Admin Dashboard</Title>
{/* Admin-only content */}
</ProtectedRoute>
);
}Permission Status UI Examples
Access Pending Page
// pages/access-pending.js
export default function AccessPending() {
return (
<div>
<Title order={1} mb="xs">⏳ Access Pending</Title>
<Text size="sm">
Your request to access this application is pending approval.
An SSO administrator will review your request shortly.
</Text>
<Text size="sm">
You will receive an email notification once your access is approved.
</Text>
<button onClick={() => window.location.href = '/logout'}>
Sign Out
</button>
</div>
);
}Access Denied Page
// pages/access-denied.js
export default function AccessDenied() {
return (
<div>
<Title order={1} mb="xs">❌ Access Denied</Title>
<Text size="sm">
Your access to this application has been revoked or denied.
</Text>
<Text size="sm">
If you believe this is an error, please contact the SSO administrator.
</Text>
<button onClick={() => window.location.href = '/logout'}>
Sign Out
</button>
</div>
);
}Two-Level Access Control Architecture
Here's how SSO-level and app-level permissions work together:
// Level 1: SSO Admin controls WHO can access the app
// -------------------------------------------------------
// SSO Admin grants user@example.com access to "myapp"
// SSO Admin assigns role: "user" or "admin"
// Level 2: Your App controls WHAT users can do inside the app
// -------------------------------------------------------
// Your app receives ID token with:
// - permissionStatus: 'approved' (derived by your backend session layer)
// - role: 'admin' (or 'user')
// Your app decides:
if (role === 'admin') {
// Grant access to:
// - User management
// - Organization settings
// - Billing
// etc.
} else if (role === 'user') {
// Grant access to:
// - View pages
// - Edit own profile
// - etc.
}
// Your app can ALSO have internal permissions:
// - Which organizations can this user access?
// - Which pages can this user edit?
// - etc.
// (These are stored in YOUR database, not in SSO)Summary
- ☑️ Always check backend-derived permission status before granting access
- ☑️ Handle
pendingandrevokedstates gracefully - ☑️ Use
rolefield to determine user's capabilities - ☑️ Implement backend middleware for permission checks
- ☑️ Provide clear UI feedback for permission states