Security

App Permissions System

Operational model and UI expectations for permission-aware access control.

SSO Version

5.29.0

Overview

The SSO service implements a two-level access control system:

  1. SSO-Level Permissions: Managed by SSO admins (who can access which apps)
  2. 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:

StatusMeaningUser Action
approvedUser has been granted access by SSO adminCan use the application
pendingUser requested access, awaiting admin approvalShow "Access Pending" message
revokedUser access was revoked by SSO adminShow "Access Denied" message

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 pending and revoked states gracefully
  • ☑️ Use role field to determine user's capabilities
  • ☑️ Implement backend middleware for permission checks
  • ☑️ Provide clear UI feedback for permission states