Skip to main content

TDD: Fix Unauthorized UI Leaks (Issue #304)

Issue: #304 - Application leaks UI elements to unauthorized users

Branch: bug/304-unauthorized-ui-leaks

Status: Planning

Date: 2025-11-21


Table of Contents

  1. Problem Statement
  2. Current Behavior
  3. Root Cause Analysis
  4. Proposed Solution
  5. Implementation Details
  6. Testing Strategy
  7. Security Considerations
  8. Files Modified

Problem Statement

When unauthorized users (not on the allowlist) visit the application, they can see UI elements that leak information about the application during the authorization check process. This creates both a security concern and a poor user experience.

Security Issues

  1. Balance Display Leak: Top app bar shows $0.00 balance, revealing billing/pricing structure
  2. Chat Functionality Leak: Chat FAB button is visible and clickable, exposing feature set
  3. UI Structure Leak: Users can infer application architecture and available features
  4. Information Disclosure: Unauthorized users should see NOTHING except the authorization error

UX Issues

  1. Project Chooser Dialog: Shows "Failed to load projects" even when user is unauthorized
  2. Confusing Error Messages: Unauthorized message appears behind the project chooser dialog
  3. Inconsistent Styling: Inline unauthorized message doesn't match application design
  4. Race Condition: UI elements flash/appear before authorization check completes

Current Behavior

Scenario: Unauthorized User Visits Application

Test User: alex@permitproof.com (not allowlisted)
Test URL: test.m3.codeproof.app

Timeline of Events:

1. User navigates to app
2. Firebase authentication succeeds ✅
3. App shows loading screen
4. Authorization check begins (isAllowListed() called)
5. ⚠️ Top app bar renders (showing $0.00 balance)
6. ⚠️ Chat FAB renders (clickable)
7. ⚠️ Project chooser dialog opens
8. Authorization check fails ❌
9. Unauthorized message appears (behind dialog)
10. ⚠️ Project chooser shows "Failed to load projects"

Screenshots from Issue

Screenshot 1: Initial State

  • Top app bar is visible with $0.00 balance
  • Chat button is visible and clickable
  • Loading state for projects

Screenshot 2: After Projects Load

  • Unauthorized error message in background
  • Project chooser dialog in foreground showing "Failed to load projects"
  • Confusing dual-error state

Root Cause Analysis

1. Template Structure Issue

File: web-ng-m3/src/app/app.component.html

<ng-container *ngIf="loading$ | async; else notLoading">
<!-- Loading screen -->
</ng-container>
<ng-template #notLoading>
<ng-container *ngIf="unauthorized$ | async; else notUnauthorized">
<!-- ISSUE: Inline unauthorized message (basic HTML) -->
<div class="unauthorized-message">
<h2>Unauthorized</h2>
<p>Your account is not allowlisted for this application.</p>
<button mat-raised-button color="primary" (click)="signOut()">Sign out</button>
</div>
</ng-container>
<ng-template #notUnauthorized>
<ng-container *ngIf="needsSignIn$ | async; else authorized">
<!-- Sign-in screen -->
</ng-container>
<ng-template #authorized>
<!-- ISSUE: All UI elements render here -->
<app-admin-top-app-bar *ngIf="..."></app-admin-top-app-bar>
<app-project-top-app-bar *ngIf="..."></app-project-top-app-bar>
<app-billing-top-app-bar *ngIf="..."></app-billing-top-app-bar>
<app-landing-top-app-bar *ngIf="..."></app-landing-top-app-bar>
<router-outlet></router-outlet>

<!-- ISSUE: Chat components render for all authorized users -->
<app-chat></app-chat>
<app-chat-fab></app-chat-fab>
</ng-template>
</ng-template>
</ng-template>

Problems:

  1. Inline unauthorized message: Uses basic HTML instead of styled UnauthorizedComponent
  2. Chat components always render: No conditional to hide during authorization check
  3. Top app bars render: All app bars (with balance info) render in authorized template
  4. Race condition: unauthorized$ may not be set before routing/dialogs occur

2. Project Chooser Race Condition

File: web-ng-m3/src/app/shared/project-routing.service.ts

async handleProjectRouting(): Promise<void> {
try {
console.log('[ProjectRoutingService] Handling project routing');
this.showProjectSelectionDialog(); // ⚠️ No authorization check!
} catch (error) {
console.error('[ProjectRoutingService] Error handling project routing:', error);
this.snackBar.open('Failed to initialize project routing', 'Close', { duration: 3000 });
}
}

Problem: Dialog opens BEFORE authorization status is confirmed

3. Landing Page Race Condition

File: web-ng-m3/src/app/components/root/landing-page/landing-page.component.ts

The landing page may trigger project routing before authorization is fully checked.

4. Authorization Flow Timing

File: web-ng-m3/src/app/shared/firebase-auth.service.ts

public async isAllowListed(email: string): Promise<boolean> {
try {
// ... gRPC call to check authorization ...
return authorized;
} catch (error: any) {
// Sets unauthorized$ or navigates to /error
this.unauthorizedSubject.next(true);
this.router.navigate(['/unauthorized'], { queryParams: { email } });
return false;
}
}

Problem: Navigation happens, but template may have already rendered UI elements


Proposed Solution

High-Level Approach

  1. Use Proper Unauthorized Route: Navigate to /unauthorized route instead of inline message
  2. Guard Against Early Rendering: Ensure UI elements don't render during authorization check
  3. Prevent Dialog Race Condition: Add authorization check before opening project chooser
  4. Clean Template Hierarchy: Remove inline unauthorized message, rely on routing

Architecture Changes

BEFORE:
┌─────────────────────────────────────────┐
│ App Component │
│ ┌─────────────────────────────────────┐ │
│ │ If unauthorized$: │ │
│ │ Show inline HTML message │ │
│ │ Else: │ │
│ │ ✓ Show all app bars (with balance)│ │
│ │ ✓ Show chat components │ │
│ │ ✓ Open project chooser │ │
│ │ → Race condition! ⚠️ │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘

AFTER:
┌─────────────────────────────────────────┐
│ App Component │
│ ┌─────────────────────────────────────┐ │
│ │ If unauthorized$: │ │
│ │ → Navigate to /unauthorized │ │
│ │ → Router shows UnauthorizedComp │ │
│ │ ✗ NO app bars │ │
│ │ ✗ NO chat components │ │
│ │ ✗ NO dialogs │ │
│ │ Else if authorized: │ │
│ │ ✓ Show all app bars │ │
│ │ ✓ Show chat components │ │
│ │ ✓ Allow project routing │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘

Implementation Details

Change 1: Fix App Component Template

File: web-ng-m3/src/app/app.component.html

Current:

<ng-container *ngIf="unauthorized$ | async; else notUnauthorized">
<div class="unauthorized-message">
<h2>Unauthorized</h2>
<p>Your account is not allowlisted for this application.</p>
<button mat-raised-button color="primary" (click)="signOut()">Sign out</button>
</div>
</ng-container>

Proposed:

<!-- Option A: Use router-outlet for all cases -->
<ng-container *ngIf="loading$ | async; else notLoading">
<app-loading title="Initializing Application..." message="Please wait, we're getting things ready."></app-loading>
</ng-container>
<ng-template #notLoading>
<ng-container *ngIf="needsSignIn$ | async; else signedIn">
<div class="signin-message">
<app-loading title="Sign In Required" message="Redirecting to Google sign-in..."></app-loading>
</div>
</ng-container>
<ng-template #signedIn>
<!-- Router handles /unauthorized route automatically -->
<!-- Only show UI elements when NOT on /unauthorized route -->
<ng-container *ngIf="!(isUnauthorizedRoute$ | async)">
<app-global-progress></app-global-progress>

<!-- All app bars -->
<app-admin-top-app-bar *ngIf="(isAdminRoute$ | async) === true" ...></app-admin-top-app-bar>
<app-project-top-app-bar *ngIf="(isProjectRoute$ | async) === true" ...></app-project-top-app-bar>
<app-billing-top-app-bar *ngIf="(isBillingRoute$ | async) === true" ...></app-billing-top-app-bar>
<app-landing-top-app-bar *ngIf="(isLandingRoute$ | async) === true" ...></app-landing-top-app-bar>

<!-- Chat components -->
<app-chat></app-chat>
<app-chat-fab></app-chat-fab>
</ng-container>

<router-outlet></router-outlet>
</ng-template>
</ng-template>

Key Changes:

  1. Remove inline unauthorized message
  2. Add isUnauthorizedRoute$ observable to detect /unauthorized route
  3. Conditionally hide all UI elements when on unauthorized route
  4. Let router handle unauthorized display via UnauthorizedComponent

Change 2: Add Unauthorized Route Detection

File: web-ng-m3/src/app/app.component.ts

Add Observable:

isUnauthorizedRoute$: Observable<boolean>;

constructor(...) {
// ... existing code ...

// Detect /unauthorized and /error routes
this.isUnauthorizedRoute$ = this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
map(() => {
const isUnauth = this.router.url.startsWith('/unauthorized') ||
this.router.url.startsWith('/error');
console.log(`[AppComponent] Unauthorized route check: ${isUnauth} (URL: "${this.router.url}")`);
return isUnauth;
}),
startWith(
this.router.url.startsWith('/unauthorized') ||
this.router.url.startsWith('/error')
),
distinctUntilChanged(),
shareReplay(1)
);
}

Change 3: Prevent Project Chooser for Unauthorized Users

File: web-ng-m3/src/app/shared/project-routing.service.ts

Current:

async handleProjectRouting(): Promise<void> {
try {
console.log('[ProjectRoutingService] Handling project routing');
this.showProjectSelectionDialog();
} catch (error) {
// ...
}
}

Proposed:

async handleProjectRouting(): Promise<void> {
try {
console.log('[ProjectRoutingService] Handling project routing');

// Check if user is authorized before showing dialog
const unauthorized = await this.firebaseAuthService.unauthorized$.pipe(take(1)).toPromise();
if (unauthorized) {
console.log('[ProjectRoutingService] User is unauthorized, skipping project routing');
return;
}

this.showProjectSelectionDialog();
} catch (error) {
console.error('[ProjectRoutingService] Error handling project routing:', error);
this.snackBar.open('Failed to initialize project routing', 'Close', { duration: 3000 });
}
}

Add Dependency Injection:

constructor(
private router: Router,
private dialog: MatDialog,
private snackBar: MatSnackBar,
private firebaseAuthService: FirebaseAuthService // Add this
) {}

Change 4: Guard Landing Page Project Routing

File: web-ng-m3/src/app/components/root/landing-page/landing-page.component.ts

Add Authorization Check:

async ngOnInit(): Promise<void> {
console.log('[LandingPageComponent] Initializing...');

// Wait for authorization check to complete
const unauthorized = await this.firebaseAuthService.unauthorized$.pipe(take(1)).toPromise();
if (unauthorized) {
console.log('[LandingPageComponent] User is unauthorized, skipping project routing');
return;
}

// Only proceed with project routing if authorized
await this.projectRoutingService.handleProjectRouting();
}

Change 5: Remove Inline Unauthorized Styles

File: web-ng-m3/src/app/app.component.css

Remove (if exists):

.unauthorized-message {
/* ... any styles for inline unauthorized message ... */
}

These styles are no longer needed since we use the styled UnauthorizedComponent.


Testing Strategy

Manual Testing Checklist

Test 1: Unauthorized User Experience

Setup:

  • Deploy to test environment
  • Use unauthorized email: alex@permitproof.com

Steps:

  1. Navigate to test.m3.codeproof.app
  2. Sign in with alex@permitproof.com
  3. Wait for authorization check

Expected Results:

  • ✅ Loading screen appears initially
  • ✅ After auth check: Only UnauthorizedComponent is visible
  • ✅ NO top app bar visible
  • ✅ NO balance display visible
  • ✅ NO chat FAB button visible
  • ✅ NO project chooser dialog appears
  • ✅ Clean, professional unauthorized message with proper styling
  • ✅ "Sign out" button works correctly

Test 2: Authorized User Experience (Regression Test)

Setup:

  • Deploy to test environment
  • Use authorized email: sanchos101@gmail.com

Steps:

  1. Navigate to test.m3.codeproof.app
  2. Sign in with authorized account
  3. Wait for initialization

Expected Results:

  • ✅ Loading screen appears initially
  • ✅ After auth check: Project chooser dialog appears
  • ✅ Top app bar IS visible (with correct balance)
  • ✅ Chat FAB button IS visible
  • ✅ Can select/create projects normally
  • ✅ All features work as before

Test 3: Route Navigation for Unauthorized Users

Setup:

  • Already signed in as unauthorized user

Steps:

  1. Manually navigate to /projects/some-id
  2. Try to access /admin
  3. Try to access /billing

Expected Results:

  • ✅ All routes redirect to /unauthorized
  • ✅ No UI leaks on any route
  • ✅ Consistent unauthorized message

Test 4: Authorization Error Handling

Setup:

  • Simulate server configuration error

Steps:

  1. Trigger authorization error (e.g., domain-wide delegation issue)

Expected Results:

  • ✅ Navigates to /error route
  • ✅ NO UI leaks
  • ✅ ServerErrorComponent displays properly

Browser Testing

Test on:

  • ✅ Chrome (latest)
  • ✅ Firefox (latest)
  • ✅ Safari (latest)
  • ✅ Mobile (Chrome/Safari)

Performance Testing

  • ✅ Verify no performance regression
  • ✅ Check that authorization check doesn't slow down app load
  • ✅ Ensure no unnecessary re-renders

Security Considerations

Information Disclosure Prevention

BEFORE (Security Issues):

  1. Balance amount revealed ($0.00) → reveals pricing structure
  2. Chat button → reveals feature availability
  3. Project chooser → reveals data structure
  4. UI patterns → allows reconnaissance

AFTER (Secure):

  1. ✅ Zero UI elements visible
  2. ✅ Only authorization message shown
  3. ✅ No inference about application structure possible
  4. ✅ Follows principle of least privilege

Defense in Depth

Even with these UI changes, we maintain backend security:

  1. ✅ All API endpoints still require authentication
  2. ✅ RBAC checks still enforced server-side
  3. ✅ Firestore rules prevent unauthorized access
  4. ✅ UI changes are additional layer, not primary security

Privacy Considerations

  • ✅ Don't log sensitive user information
  • ✅ Don't reveal why user is unauthorized (could be privacy issue)
  • ✅ Generic error messages prevent information disclosure

Files Modified

Primary Changes

  1. web-ng-m3/src/app/app.component.html

    • Remove inline unauthorized message
    • Add conditional hiding of UI elements
    • Let router handle unauthorized display
  2. web-ng-m3/src/app/app.component.ts

    • Add isUnauthorizedRoute$ observable
    • Update constructor to initialize new observable
  3. web-ng-m3/src/app/shared/project-routing.service.ts

    • Add authorization check before opening dialog
    • Inject FirebaseAuthService
    • Early return if unauthorized
  4. web-ng-m3/src/app/components/root/landing-page/landing-page.component.ts

    • Add authorization check in ngOnInit()
    • Skip project routing if unauthorized
  5. web-ng-m3/src/app/app.component.css

    • Remove inline unauthorized message styles (if any)

No Changes Required

  1. web-ng-m3/src/app/components/root/unauthorized/unauthorized.component.html

    • Already properly styled ✅
  2. web-ng-m3/src/app/components/root/unauthorized/unauthorized.component.scss

    • Already has good styling ✅
  3. web-ng-m3/src/app/shared/firebase-auth.service.ts

    • Already handles authorization correctly ✅

Timeline

Estimated Effort: 2-3 hours

  1. Implementation: 1 hour

    • Template changes: 20 min
    • TypeScript changes: 30 min
    • Testing locally: 10 min
  2. Testing: 1 hour

    • Manual testing: 30 min
    • Browser testing: 20 min
    • Regression testing: 10 min
  3. Deployment: 30 min

    • Build verification: 10 min
    • Deploy to test environment: 10 min
    • Final verification: 10 min
  4. Buffer: 30 min


Success Criteria

✅ Unauthorized users see ONLY the UnauthorizedComponent
✅ NO top app bar visible for unauthorized users
✅ NO chat FAB visible for unauthorized users
✅ NO project chooser dialog for unauthorized users
✅ Clean, professional unauthorized message
✅ No information leakage about application structure
✅ Authorized users experience no regression
✅ All existing functionality works correctly


References

  • Issue #304
  • Angular Material Documentation
  • Firebase Auth Documentation
  • OWASP Information Disclosure Guidelines

Approval

  • Technical approach approved
  • Security implications reviewed
  • UX improvements verified
  • Ready for implementation