Skip to content

Checking access...

RBAC Integration Guide

This guide provides comprehensive documentation for integrating Role-Based Access Control (RBAC) into canisters and frontend suites within the Hello World DAO platform.

Table of Contents

Role Model Overview

The platform implements a five-tier role hierarchy managed by the auth-service canister:

RoleDescriptionDefault AssignmentTypical Permissions
AdminFull system accessManually assigned by existing Admin or controllerKYC review, member management, system monitoring, governance oversight, treasury management, content moderation
ModeratorContent and community managementManually assigned by AdminContent moderation, community discussions, reporting
DeveloperPlatform developers and technical contributorsManually assigned by AdminArchitecture docs, API reference, technical documentation
MemberStandard authenticated userAssigned on registration or membership verificationCreate proposals, vote on proposals, view treasury, participate in discussions
UserNon-member authenticated userDefault session role for users with no explicit rolesBasic platform access, view public content, register for membership

Role Assignment Rules:

  • Sessions for users with no explicit roles default to User (non-member public user)
  • Member role is assigned upon successful registration or membership verification
  • Moderator and Admin roles must be explicitly assigned by an existing Admin or the canister controller
  • Roles are additive: a user can have multiple roles (e.g., Admin + Moderator + Member)
  • Role changes take effect on the next login (roles are cached in the session token)

Auth-Service Candid API

The auth-service canister provides the following role management methods:

assign_role

Assigns a role to a user. Only callable by Admin users or the canister controller.

Signature:

candid
assign_role : (user_id: text, role: Role) -> (Result)

Parameters:

  • user_id - Principal ID of the user as a text string
  • role - Role variant: variant { Admin }, variant { Moderator }, variant { Developer }, variant { Member }, or variant { User }

Returns:

  • variant { Ok } - Role successfully assigned
  • variant { Err: text } - Error message (e.g., "Unauthorized", "User not found")

Example (dfx CLI):

bash
# Assign Admin role to a user
dfx canister call auth-service assign_role '("2vxsx-fae", variant { Admin })' --network ic

# Assign Moderator role
dfx canister call auth-service assign_role '("2vxsx-fae", variant { Moderator })' --network ic

# Assign Developer role
dfx canister call auth-service assign_role '("2vxsx-fae", variant { Developer })' --network ic

Error Codes:

  • "Unauthorized" - Caller is not an Admin or controller
  • "User not found" - User ID does not exist in the system
  • "Role already assigned" - User already has the specified role

revoke_role

Revokes a role from a user. Only callable by Admin users or the canister controller.

Signature:

candid
revoke_role : (user_id: text, role: Role) -> (Result)

Parameters:

  • user_id - Principal ID of the user as a text string
  • role - Role variant to revoke: variant { Admin }, variant { Moderator }, variant { Developer }, variant { Member }, or variant { User }

Returns:

  • variant { Ok } - Role successfully revoked
  • variant { Err: text } - Error message

Example (dfx CLI):

bash
# Revoke Admin role from a user
dfx canister call auth-service revoke_role '("2vxsx-fae", variant { Admin })' --network ic

Error Codes:

  • "Unauthorized" - Caller is not an Admin or controller
  • "User not found" - User ID does not exist
  • "Role not assigned" - User does not have the specified role

Important: Revoking a role does not immediately affect active sessions. The user's session will continue with the old roles until they log in again.

get_user_roles

Retrieves all roles assigned to a user. Query method (read-only).

Signature:

candid
get_user_roles : (user_id: text) -> (vec Role) query

Parameters:

  • user_id - Principal ID of the user as a text string

Returns:

  • Vector of Role variants (e.g., [variant { Admin }, variant { Member }])

Example (dfx CLI):

bash
# Get all roles for a user
dfx canister call auth-service get_user_roles '("2vxsx-fae")' --network ic --query

Note: Returns an empty vector [] if the user has no roles or does not exist.

has_role

Checks if a user has a specific role. Query method (read-only).

Signature:

candid
has_role : (user_id: text, role: Role) -> (bool) query

Parameters:

  • user_id - Principal ID of the user as a text string
  • role - Role variant to check: variant { Admin }, variant { Moderator }, variant { Developer }, variant { Member }, or variant { User }

Returns:

  • true - User has the specified role
  • false - User does not have the role (or user does not exist)

Example (dfx CLI):

bash
# Check if a user is an Admin
dfx canister call auth-service has_role '("2vxsx-fae", variant { Admin })' --network ic --query

validate_session_with_role

Validates a session token and checks if the user has a required role. Query method (read-only).

Signature:

candid
validate_session_with_role : (access_token: text, required_role: text) -> (Result_SessionInfo) query

Parameters:

  • access_token - Session token to validate
  • required_role - Required role as lowercase string: "admin", "moderator", "developer", "member", or "user"

Returns:

  • variant { Ok: SessionInfo } - Session valid and user has the required role
    • SessionInfo contains: user_id, email, roles (vec text), created_at, expires_at
  • variant { Err: text } - Error message

Example (Rust inter-canister call):

rust
use ic_cdk::api::call::call;

#[update]
async fn admin_only_method(caller_token: String) -> Result<String, String> {
    // Validate that the caller has Admin role
    let result: Result<(Result<SessionInfo, String>,), _> = call(
        auth_service_principal,
        "validate_session_with_role",
        (caller_token, "admin".to_string()),
    ).await;

    match result {
        Ok((Ok(session_info),)) => {
            // User is authenticated and has Admin role
            Ok(format!("Admin action performed by {}", session_info.user_id))
        }
        Ok((Err(e),)) => Err(format!("Unauthorized: {}", e)),
        Err((code, msg)) => Err(format!("Call failed: {:?} - {}", code, msg)),
    }
}

Error Codes:

  • "Invalid token" - Token is malformed or expired
  • "Session not found" - Token does not correspond to an active session
  • "Role not found" - User does not have the required role
  • "Session expired" - Token has passed its expiration time

Use Cases:

  • Backend canisters can validate that a caller has a specific role before executing sensitive operations
  • Eliminates the need to store role information in every canister
  • Centralizes role management in the auth-service

Frontend Integration

The @hello-world-co-op/auth package (v0.2.0+) provides React components and hooks for role-based UI rendering and route protection.

Role Propagation Flow

mermaid
sequenceDiagram
    participant Suite as Frontend Suite
    participant OB as oracle-bridge
    participant AS as auth-service
    participant Store as nanostores
    participant Component as React Component

    Suite->>OB: GET /api/auth/session (cookie)
    OB->>AS: validate_session
    AS-->>OB: SessionInfo + roles
    OB-->>Suite: { authenticated: true, user_id, roles: ["admin", "member"] }
    Suite->>Store: Update $userRoles atom
    Store->>Component: useRoles() reads $userRoles
    Component->>Component: Render based on roles

useRoles Hook

The useRoles() hook provides access to the current user's roles.

Import:

typescript
import { useRoles } from '@hello-world-co-op/auth';

Usage:

typescript
function AdminPanel() {
  const { roles, isLoading, hasRole, isAdmin } = useRoles();

  if (isLoading) {
    return <div>Loading permissions...</div>;
  }

  if (!isAdmin) {
    return <div>Access Denied</div>;
  }

  return (
    <div>
      <h1>Admin Panel</h1>
      {hasRole('moderator') && <ModeratorTools />}
    </div>
  );
}

Return Value:

typescript
{
  roles: string[];        // Array of role names: ["admin", "developer", "member"]
  hasRole: (role: string) => boolean;  // Check if user has a specific role
  isAdmin: boolean;       // Convenience: true if user has "admin" role
}
// Note: For loading state, use useAuth() which provides isLoading

Role Name Format:

  • Role names are always lowercase strings: "admin", "moderator", "developer", "member", "user"
  • The hasRole() comparison is case-sensitive — always use lowercase

RoleGuard Component

The <RoleGuard> component conditionally renders children based on role requirements.

Import:

typescript
import { RoleGuard } from '@hello-world-co-op/auth';

Basic Usage:

typescript
function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* Only visible to Admin users */}
      <RoleGuard requiredRole="admin">
        <AdminSection />
      </RoleGuard>

      {/* Visible to Moderator users */}
      <RoleGuard requiredRole="moderator">
        <ModerationPanel />
      </RoleGuard>

      {/* For multiple roles, use hasRole() from useRoles() hook */}
    </div>
  );
}

Props:

typescript
interface RoleGuardProps {
  requiredRole: string;           // Required role name (e.g., "admin")
  fallback?: React.ReactNode;     // Content to show if role check fails (default: null)
  children: React.ReactNode;      // Protected content
}

With Fallback Content:

typescript
<RoleGuard
  requiredRole="admin"
  fallback={<div>Admin access required</div>}
>
  <AdminTools />
</RoleGuard>

Route Protection:

typescript
// In App.tsx or routing configuration
<Route
  path="/admin/*"
  element={
    <RoleGuard requiredRole="admin" fallback={<Navigate to="/unauthorized" />}>
      <AdminDashboard />
    </RoleGuard>
  }
/>

Important: RoleGuard is a UI convenience only. Always verify roles server-side in canister methods. Frontend guards can be bypassed by users modifying client-side code.

hasRole and isAdmin

The hasRole() function and isAdmin boolean are available from the useRoles() or useAuth() hooks. They are not standalone exports — they require React context.

Usage:

typescript
import { useRoles } from '@hello-world-co-op/auth';

function AdminPanel() {
  const { hasRole, isAdmin } = useRoles();

  // Single role check
  if (!isAdmin) return <AccessDenied />;

  // Multiple role check
  const canModerate = hasRole('admin') || hasRole('moderator');

  return <div>{canModerate && <ModerationTools />}</div>;
}

Signature:

typescript
hasRole(role: string): boolean  // Case-sensitive; roles are always lowercase
isAdmin: boolean                // Equivalent to hasRole('admin')

Note: This function reads from the global $userRoles nanostore atom. It should only be used after authentication has completed and roles have been loaded.

isAdmin Convenience

The isAdmin boolean is available from useRoles() as a shorthand for hasRole('admin').

Usage:

typescript
const { isAdmin } = useRoles();

if (isAdmin) {
  // Show admin-only UI
}

This is equivalent to:

typescript
const { hasRole } = useRoles();

if (hasRole('admin')) {
  // Show admin-only UI
}

Role State Management

Roles are stored in the $userRoles nanostore atom and synchronized with the authentication session:

typescript
// src/stores/auth.ts
import { atom } from 'nanostores';

export const $userRoles = atom<string[]>([]);

// Updated when session is fetched from oracle-bridge
export async function fetchSession() {
  const response = await fetch(`${ORACLE_BRIDGE_URL}/api/auth/session`, {
    credentials: 'include',
  });

  if (response.ok) {
    const data = await response.json();
    $userRoles.set(data.roles || []);  // ["admin", "member"]
  }
}

Canister Integration

Backend canisters can validate that a caller has a specific role by calling auth-service::validate_session_with_role.

Inter-Canister Role Check Pattern

Example: Admin-Only Canister Method

rust
use ic_cdk::api::call::call;
use candid::{CandidType, Deserialize, Principal};

#[derive(CandidType, Deserialize)]
struct SessionInfo {
    user_id: String,
    email: String,
    roles: Vec<String>,
    created_at: u64,
    expires_at: u64,
}

#[update]
async fn approve_kyc_submission(
    access_token: String,
    submission_id: String,
) -> Result<String, String> {
    // Validate that the caller has Admin role
    let auth_service_id = Principal::from_text(AUTH_SERVICE_CANISTER_ID)
        .map_err(|e| format!("Invalid auth service ID: {}", e))?;

    let result: Result<(Result<SessionInfo, String>,), _> = call(
        auth_service_id,
        "validate_session_with_role",
        (access_token.clone(), "admin".to_string()),
    ).await;

    match result {
        Ok((Ok(session_info),)) => {
            // User is authenticated and has Admin role
            ic_cdk::println!("Admin {} approved KYC {}", session_info.user_id, submission_id);

            // Perform admin operation
            approve_submission_internal(submission_id)
        }
        Ok((Err(e),)) => {
            // Auth check failed
            Err(format!("Access denied: {}", e))
        }
        Err((code, msg)) => {
            // Inter-canister call failed
            Err(format!("Auth service error: {:?} - {}", code, msg))
        }
    }
}

fn approve_submission_internal(submission_id: String) -> Result<String, String> {
    // Actual business logic here
    Ok(format!("Submission {} approved", submission_id))
}

Role-Gated Method Examples

Multiple Roles Accepted:

rust
#[update]
async fn moderate_content(
    access_token: String,
    content_id: String,
    action: String,
) -> Result<String, String> {
    // Accept either Admin or Moderator role
    let auth_service_id = Principal::from_text(AUTH_SERVICE_CANISTER_ID)?;

    // Try Moderator role first
    let moderator_check: Result<(Result<SessionInfo, String>,), _> = call(
        auth_service_id,
        "validate_session_with_role",
        (access_token.clone(), "moderator".to_string()),
    ).await;

    if let Ok((Ok(session_info),)) = moderator_check {
        return moderate_content_internal(content_id, action, session_info.user_id);
    }

    // Fall back to Admin role
    let admin_check: Result<(Result<SessionInfo, String>,), _> = call(
        auth_service_id,
        "validate_session_with_role",
        (access_token, "admin".to_string()),
    ).await;

    match admin_check {
        Ok((Ok(session_info),)) => {
            moderate_content_internal(content_id, action, session_info.user_id)
        }
        _ => Err("Access denied: Moderator or Admin role required".to_string()),
    }
}

Member-Level Access (Default):

rust
#[update]
async fn create_proposal(
    access_token: String,
    title: String,
    description: String,
) -> Result<String, String> {
    // Any authenticated member can create proposals
    let auth_service_id = Principal::from_text(AUTH_SERVICE_CANISTER_ID)?;

    let result: Result<(Result<SessionInfo, String>,), _> = call(
        auth_service_id,
        "validate_session_with_role",
        (access_token, "member".to_string()),
    ).await;

    match result {
        Ok((Ok(session_info),)) => {
            create_proposal_internal(title, description, session_info.user_id)
        }
        Ok((Err(e),)) => Err(format!("Authentication required: {}", e)),
        Err((code, msg)) => Err(format!("Auth service error: {:?} - {}", code, msg)),
    }
}

Error Handling Best Practices

Fail-Closed Approach:

rust
// WRONG - fails open (allows access on error)
let is_admin = match check_admin_role(token).await {
    Ok(true) => true,
    _ => false,  // Treats errors as "not admin" - could allow unintended access
};

// CORRECT - fails closed (denies access on error)
let session_info = match validate_admin_role(token).await {
    Ok(info) => info,
    Err(e) => return Err(format!("Access denied: {}", e)),
};
// Only proceed if validation succeeded

Logging Unauthorized Attempts:

rust
#[update]
async fn admin_method(access_token: String) -> Result<String, String> {
    let result = validate_admin_role(access_token.clone()).await;

    match result {
        Ok(session_info) => {
            // Success - proceed
            perform_admin_action(session_info.user_id)
        }
        Err(e) => {
            // Log unauthorized attempt
            ic_cdk::println!(
                "SECURITY: Unauthorized admin access attempt - token: {} error: {}",
                &access_token[..8],  // Log only token prefix for security
                e
            );
            Err(format!("Access denied: {}", e))
        }
    }
}

Migration Guide

Follow these steps to upgrade an existing suite from authentication-only to RBAC support.

Step 1: Update @hello-world-co-op/auth to ^0.2.0

In package.json:

json
{
  "dependencies": {
    "@hello-world-co-op/auth": "^0.2.0"
  }
}

Install the update:

bash
npm install

Verify the version:

bash
npm list @hello-world-co-op/auth

Expected output:

@hello-world-co-op/auth@0.2.0

Step 2: Wrap App in AuthProvider (if not already)

If your suite uses the Auth Bridge pattern (think-tank, dao, dao-admin), ensure AuthProviderBridge is wrapping your app root.

In src/App.tsx or src/main.tsx:

typescript
import { AuthProviderBridge } from './components/auth/AuthProviderBridge';

function App() {
  return (
    <AuthProviderBridge>
      {/* Your routes and components */}
    </AuthProviderBridge>
  );
}

For suites using auth directly (governance-suite): Roles are automatically loaded from the session endpoint. No additional setup required.

Step 3: Use RoleGuard for Protected Routes

Replace custom auth guards or admin checks with RoleGuard:

Before (custom guard):

typescript
// src/components/AdminGuard.tsx (custom implementation)
function AdminGuard({ children }: { children: React.ReactNode }) {
  const { user } = useAuth();

  if (!user || user.role !== 'admin') {
    return <Navigate to="/unauthorized" />;
  }

  return <>{children}</>;
}

// In routes
<Route path="/admin/*" element={<AdminGuard><AdminDashboard /></AdminGuard>} />

After (using RoleGuard):

typescript
import { RoleGuard } from '@hello-world-co-op/auth';

// In routes
<Route
  path="/admin/*"
  element={
    <RoleGuard requiredRole="admin" fallback={<Navigate to="/unauthorized" />}>
      <AdminDashboard />
    </RoleGuard>
  }
/>

Remove custom guard file:

bash
rm src/components/AdminGuard.tsx

Step 4: Use useRoles for Conditional UI

Replace custom role checks with the useRoles() hook:

Before:

typescript
import { useAuth } from '@hello-world-co-op/auth';

function Dashboard() {
  const { user } = useAuth();
  const isAdmin = user?.role === 'admin';

  return (
    <div>
      <h1>Dashboard</h1>
      {isAdmin && <AdminPanel />}
    </div>
  );
}

After:

typescript
import { useRoles } from '@hello-world-co-op/auth';

function Dashboard() {
  const { isAdmin } = useRoles();

  return (
    <div>
      <h1>Dashboard</h1>
      {isAdmin && <AdminPanel />}
    </div>
  );
}

Step 5: Update Canister Methods (if applicable)

If your suite calls backend canister methods that require role verification, update the method signatures to accept an access_token parameter and call validate_session_with_role.

Before (no role check):

rust
#[update]
fn approve_action(action_id: String) -> Result<String, String> {
    // Anyone can call this - no auth check!
    approve_action_internal(action_id)
}

After (with role check):

rust
#[update]
async fn approve_action(
    access_token: String,
    action_id: String,
) -> Result<String, String> {
    // Verify caller has Admin role
    validate_admin_role(access_token).await?;
    approve_action_internal(action_id)
}

async fn validate_admin_role(token: String) -> Result<SessionInfo, String> {
    let auth_service_id = Principal::from_text(AUTH_SERVICE_CANISTER_ID)
        .map_err(|e| format!("Auth service ID error: {}", e))?;

    let result: Result<(Result<SessionInfo, String>,), _> = call(
        auth_service_id,
        "validate_session_with_role",
        (token, "admin".to_string()),
    ).await;

    match result {
        Ok((Ok(info),)) => Ok(info),
        Ok((Err(e),)) => Err(format!("Access denied: {}", e)),
        Err((code, msg)) => Err(format!("Auth error: {:?} - {}", code, msg)),
    }
}

Step 6: Update Frontend API Calls

Update frontend service functions to pass the access token to backend methods:

Before:

typescript
// src/services/adminService.ts
export async function approveAction(actionId: string): Promise<ApiResponse<string>> {
  const actor = await createActor('admin-canister-id');
  const result = await actor.approve_action(actionId);
  return handleApiResponse(result);
}

After:

typescript
// src/services/adminService.ts
export async function approveAction(actionId: string): Promise<ApiResponse<string>> {
  const token = getAccessToken();  // Get token from auth store
  if (!token) {
    return { success: false, error: 'Not authenticated' };
  }

  const actor = await createActor('admin-canister-id');
  const result = await actor.approve_action(token, actionId);
  return handleApiResponse(result);
}

function getAccessToken(): string | null {
  // Get token from auth store/cookie
  return localStorage.getItem('access_token');
}

Migration Checklist

  • [ ] Updated @hello-world-co-op/auth to v0.2.0+
  • [ ] Verified AuthProviderBridge wraps app root (for Auth Bridge suites)
  • [ ] Replaced custom admin guards with <RoleGuard requiredRole="admin">
  • [ ] Updated conditional UI to use useRoles() hook
  • [ ] Updated canister methods to call validate_session_with_role
  • [ ] Updated frontend service calls to pass access tokens
  • [ ] Removed custom role management code
  • [ ] Tested admin/moderator/member access flows
  • [ ] Updated suite README with RBAC notes

Security Considerations

Fail-Closed Design

Always deny access if role verification fails or errors occur.

rust
// CORRECT - fail closed
match validate_role(token).await {
    Ok(session_info) => perform_action(session_info),
    Err(_) => return Err("Access denied".to_string()),  // Deny on error
}

// WRONG - fail open
let allowed = match validate_role(token).await {
    Ok(_) => true,
    Err(_) => false,  // Could allow unintended access if validation service is down
};
if allowed {
    perform_action();
}

Role Caching in Sessions

Roles are cached in session tokens at login time. This has important implications:

  • Performance: Role checks are fast (no database lookup per request)
  • Staleness: Role changes do not take effect until the user logs in again
  • Session Duration: Current sessions have a 24-hour lifetime

Example Scenario:

  1. User logs in at 9:00 AM → Session created with roles: ["member"]
  2. Admin promotes user to Moderator at 10:00 AM → Role stored in auth-service
  3. User still sees Member-only UI until they log out and back in
  4. User logs in again at 2:00 PM → New session with roles: ["moderator", "member"]

Mitigation: For time-sensitive role changes (e.g., emergency role revocation), invalidate the user's session to force re-authentication:

bash
# Force session refresh by invalidating current session (requires backend support)
dfx canister call auth-service invalidate_user_sessions '("user-id")' --network ic

Session Expiry and Role Staleness

Session tokens expire after 24 hours. After expiration:

  • The session is no longer valid
  • Role checks fail with "Session expired"
  • User must log in again to get a fresh session with current roles

Best Practice: Monitor session expiry in the frontend and prompt users to re-authenticate before token expiration:

typescript
// src/stores/auth.ts
export function checkSessionExpiry() {
  const session = $session.get();
  if (!session) return;

  const now = Date.now();
  const expiresAt = session.expires_at * 1000; // Convert to milliseconds
  const timeUntilExpiry = expiresAt - now;

  // Warn user 5 minutes before expiry
  if (timeUntilExpiry < 5 * 60 * 1000 && timeUntilExpiry > 0) {
    showNotification('Your session is about to expire. Please save your work.');
  }

  // Force refresh if expired
  if (timeUntilExpiry <= 0) {
    logout();
    navigateToLogin();
  }
}

Server-Side Audit Logging

Always log unauthorized access attempts for security monitoring:

rust
#[update]
async fn admin_method(access_token: String) -> Result<String, String> {
    match validate_admin_role(access_token.clone()).await {
        Ok(session_info) => {
            ic_cdk::println!("AUDIT: Admin action by {}", session_info.user_id);
            perform_admin_action()
        }
        Err(e) => {
            // Log unauthorized attempt
            ic_cdk::println!(
                "SECURITY: Unauthorized admin access attempt - token prefix: {} error: {}",
                &access_token[..8],
                e
            );
            Err(format!("Access denied: {}", e))
        }
    }
}

Audit Log Fields:

  • Timestamp (automatic via IC system time)
  • User ID or token prefix
  • Method called
  • Role required
  • Success/failure
  • Error message if failed

Frontend Guards are UX Only

Critical: Frontend role guards (RoleGuard, useRoles) are UI conveniences only. They improve user experience by hiding irrelevant UI, but they do NOT provide security.

Why Frontend Guards are Not Secure:

  • Users can modify client-side JavaScript
  • Browser DevTools can bypass React components
  • Network requests can be crafted manually (e.g., via curl)

Always Verify Server-Side:

rust
// Backend canister method - ALWAYS CHECK ROLE HERE
#[update]
async fn admin_action(access_token: String, data: String) -> Result<String, String> {
    // This is the REAL security check
    validate_admin_role(access_token).await?;

    // Safe to proceed
    perform_admin_operation(data)
}

Frontend:

typescript
// Frontend component - UX convenience only
function AdminPanel() {
  const { isAdmin } = useRoles();

  // This hides the UI, but doesn't secure the backend
  if (!isAdmin) {
    return <div>Access Denied</div>;
  }

  return (
    <button onClick={callAdminAction}>
      Admin Action
    </button>
  );
}

async function callAdminAction() {
  // Backend will verify role again - this is the REAL check
  const result = await adminService.performAction(token, data);
}

Token Storage Security

Access tokens should be stored securely:

Recommended (HTTP-only cookie):

typescript
// Token is in HTTP-only cookie (set by oracle-bridge)
// Not accessible to JavaScript - reduces XSS risk
await fetch(`${API_URL}/api/admin/action`, {
  credentials: 'include',  // Sends cookie automatically
});

Acceptable (localStorage with precautions):

typescript
// If using localStorage (less secure but sometimes necessary)
const token = localStorage.getItem('access_token');

// ALWAYS validate token format before using
if (!token || token.length < 20) {
  throw new Error('Invalid token');
}

// NEVER log the full token
console.log('Token prefix:', token.substring(0, 8));

Never:

  • Store tokens in URL parameters
  • Store tokens in sessionStorage (vulnerable to XSS like localStorage, but also lost on tab close)
  • Log full tokens to console
  • Send tokens via GET query parameters

Rate Limiting and Abuse Prevention

Implement rate limiting on sensitive role-protected endpoints:

rust
use std::collections::HashMap;
use ic_cdk::api::time;

thread_local! {
    static RATE_LIMIT: RefCell<HashMap<String, Vec<u64>>> = RefCell::new(HashMap::new());
}

fn check_rate_limit(user_id: &str, max_calls: usize, window_seconds: u64) -> Result<(), String> {
    let now = time() / 1_000_000_000; // Convert to seconds

    RATE_LIMIT.with(|rl| {
        let mut map = rl.borrow_mut();
        let calls = map.entry(user_id.to_string()).or_insert_with(Vec::new);

        // Remove old calls outside the time window
        calls.retain(|&timestamp| now - timestamp < window_seconds);

        if calls.len() >= max_calls {
            return Err(format!("Rate limit exceeded: {} calls per {} seconds", max_calls, window_seconds));
        }

        calls.push(now);
        Ok(())
    })
}

#[update]
async fn admin_action(access_token: String, data: String) -> Result<String, String> {
    let session_info = validate_admin_role(access_token).await?;

    // Rate limit: 10 calls per minute
    check_rate_limit(&session_info.user_id, 10, 60)?;

    perform_admin_operation(data)
}

Hello World Co-Op DAO