Skip to content

Authentication Architecture

This document covers the authentication architecture in JunctionRelay, including how cloud authentication works, token management, and a case study of a bug fix related to backend authentication status.

Overview

JunctionRelay supports three authentication modes:

  • None: No authentication required
  • Local: Username/password authentication stored locally in SQLite
  • Cloud: OAuth-based authentication via JunctionRelay Cloud

This document focuses primarily on cloud authentication and how the backend manages cloud sessions.

Cloud Authentication Architecture

How Cloud Authentication Works

When a user authenticates with JunctionRelay Cloud:

  1. OAuth Flow: User is redirected to JunctionRelay Cloud for OAuth login
  2. Token Exchange: After successful login, the cloud returns:
  3. accessToken - Short-lived JWT token for API requests
  4. refreshToken - Long-lived token for obtaining new access tokens
  5. userId - Cloud user identifier embedded in the JWT
  6. Backend Storage: The backend stores these credentials in its SQLite database
  7. Persistent Session: Tokens persist across browser sessions and backend restarts

Backend vs Frontend Authentication

It's crucial to understand the distinction between two authentication states:

Property Meaning Checked Against
isAuthenticated Frontend user has a valid session Frontend session token or cloud token in auth header
backendAuthenticated Backend has stored cloud credentials and can communicate with cloud API Backend's stored tokens in database

Key Concept: The backend does NOT have its own separate cloud account. Instead, it stores and operates using the credentials of whichever user last logged in via OAuth.

Token Storage and Management

Database Schema

CREATE TABLE CloudSessions (
    Id INTEGER PRIMARY KEY AUTOINCREMENT,
    UserId TEXT NOT NULL,
    BackendId TEXT NOT NULL,
    EncryptedRefreshToken TEXT NOT NULL,
    CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
    UpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(UserId, BackendId)
);
  • UserId: Cloud user ID from the JWT token
  • BackendId: Unique identifier for this backend instance
  • EncryptedRefreshToken: Encrypted refresh token for token renewal

Service_CloudSessionStore

The Service_CloudSessionStore class manages token lifecycle:

Key Methods: - StoreSession() - Stores user's cloud credentials - GetValidAccessTokenAsync() - Retrieves access token, auto-refreshing if expired - RefreshTokenInternalAsync() - Contacts cloud API to refresh tokens - ClearSession() - Removes stored credentials on logout

Auto-Refresh Logic:

public async Task<string?> GetValidAccessTokenAsync(CancellationToken cancellationToken = default)
{
    // If we have a valid access token, return it
    if (!string.IsNullOrEmpty(_encryptedAccessToken) &&
        _tokenExpiryTime.HasValue &&
        _tokenExpiryTime.Value > DateTime.UtcNow.AddMinutes(5))
    {
        return _secretsService.DecryptSecret(_encryptedAccessToken);
    }

    // Token expired or missing - trigger refresh
    _refreshTask = RefreshTokenInternalAsync(cancellationToken);
    var refreshedToken = await _refreshTask;
    return refreshedToken;
}

When the access token expires (or is within 5 minutes of expiration), the backend automatically uses the refresh token to obtain a new access token from the cloud API, maintaining continuous cloud connectivity.

The cloud sync icon in the navbar (visible in cloud mode) indicates the backend's cloud authentication status:

  • Green: Backend has valid cloud credentials and can successfully communicate with cloud API
  • Red: Backend does NOT have valid cloud credentials OR cannot reach cloud API

This is determined by the backendAuthenticated property returned from /api/unified-auth/status.

Bug Fix Case Study: Navbar Showing Red After Token Refresh

Problem Statement

Issue: The navbar cloud sync icon incorrectly displayed red (indicating no backend authentication) even though: - The backend had valid, stored cloud credentials - Token auto-refresh was working correctly - The cloud backend showed an active session refreshed recently - Backend console showed no errors

User Impact: Users were concerned their backend had lost cloud connectivity when it was actually working fine.

Root Cause Analysis

The issue was in how the backend validated its cloud connection:

Original Flow: 1. Frontend requests /api/unified-auth/status 2. Backend calls GetAuthStatusAsync() 3. GetAuthStatusAsync() calls GetUserInfoAsync() with the backend's token 4. GetUserInfoAsync() proxies the request to cloud API and caches the result for 2 minutes 5. Cache key is generated based on userId (from JWT token)

The Problem: When the backend auto-refreshed its token: 1. Old access token expires 2. Backend calls cloud API → gets 400 or 401 error 3. Error response is cached with key proxy:auth/user-info:uid:user_xxx 4. Backend successfully refreshes to new access token 5. Frontend checks status → cache returns old 400 error (same userId, same cache key) 6. Navbar shows red icon even though new token is valid

Why the cache key was the issue: - Cache keys were based on userId extracted from the JWT token - When a token is refreshed, the new token has the same userId as the old token - So the cache key remained identical: proxy:auth/user-info:uid:user_xxx - The cached error response was returned even with the new, valid token

Solution Implemented

Created a non-cached validation method specifically for checking backend authentication status:

File: Services/Service_CloudAuth.cs

// New non-cached version for backend token validation
private async Task<IActionResult> ValidateBackendTokenAsync(string backendToken)
    => await ProxyCloudRequest("auth/user-info", HttpMethod.Get, $"Bearer {backendToken}", cacheDuration: null);

public async Task<IActionResult> GetAuthStatusAsync(string? authHeader)
{
    var backendToken = await _cloudSessionStore.GetValidAccessTokenAsync();
    var userId = _cloudSessionStore.GetUserId();

    if (string.IsNullOrEmpty(backendToken) || string.IsNullOrEmpty(userId))
    {
        return backendAuthenticated: false;
    }

    // Use non-cached validation to avoid stale cache after token refresh
    var userInfoResult = await ValidateBackendTokenAsync(backendToken);

    if (userInfoResult is OkObjectResult okResult && okResult.Value is JsonElement userInfo)
    {
        return backendAuthenticated: true;
    }
    else
    {
        return backendAuthenticated: false;
    }
}

Key Changes: - Added ValidateBackendTokenAsync() that passes cacheDuration: null to skip caching - Modified GetAuthStatusAsync() to use the non-cached validation method - Frontend-requested GetUserInfoAsync() still uses caching (2 minutes) for performance - Backend status checks always use fresh validation

Testing Endpoint

A testing endpoint was added to force token refresh on demand:

Endpoint: POST /api/unified-auth/tokens/force-refresh

Purpose: - Clears the current access token (preserves refresh token) - Forces immediate token refresh - Returns information about the refresh result

Implementation:

// File: Services/Service_CloudSessionStore.cs
public async Task<(bool success, string? message, string? newToken)> ForceRefreshTokenAsync()
{
    // Clear access token to force refresh
    ClearAccessTokenOnly();

    try
    {
        var newToken = await GetValidAccessTokenAsync();
        if (!string.IsNullOrEmpty(newToken))
        {
            return (true, "Token refreshed successfully", newToken);
        }
        else
        {
            return (false, "Token refresh failed - no token returned", null);
        }
    }
    catch (Exception ex)
    {
        return (false, $"Token refresh failed: {ex.Message}", null);
    }
}

Usage:

POST http://localhost:7180/api/unified-auth/tokens/force-refresh

Files Modified

  1. Services/Service_CloudAuth.cs
  2. Lines 265-266: Added ValidateBackendTokenAsync() method
  3. Line 308: Changed to use non-cached validation

  4. Services/Service_CloudSessionStore.cs

  5. Lines 162-189: Added ForceRefreshTokenAsync() for testing

  6. Controllers/Controller_UnifiedAuth.cs

  7. Lines 309-342: Added force refresh endpoint

Impact and Benefits

Before Fix: - Navbar showed red icon incorrectly after token auto-refresh - Users confused about backend cloud connectivity status - Had to manually log out and back in to clear cached error

After Fix: - Navbar immediately reflects correct backend authentication status - Auto-refresh works seamlessly with accurate status indication - No user intervention required when tokens refresh

Performance Consideration: - Non-cached validation means every status check makes a cloud API call - This is acceptable because: - Status endpoint is only called when navbar loads/refreshes (not frequently) - Accurate real-time status is more important than caching - Frontend user-info requests still use caching for performance

Key Takeaways

  1. Backend Stores User Credentials: The backend doesn't have its own cloud account; it stores and operates using the logged-in user's credentials

  2. Auto-Refresh is Automatic: When access tokens expire, the backend automatically refreshes them using the stored refresh token without user intervention

  3. Cache Keys Matter: When caching API responses, ensure cache keys properly account for scenarios where the underlying data changes but the key remains the same

  4. Separate Validation Paths: Different use cases (backend status vs frontend user info) may require different caching strategies

  5. Testing Endpoints: Adding developer testing endpoints (like force-refresh) helps validate fixes and debug issues

Implementation Details

For developers working on authentication features:

Adding New Cloud-Authenticated Endpoints

When creating endpoints that require backend cloud authentication:

public async Task<IActionResult> MyCloudEndpoint()
{
    var backendToken = await _cloudSessionStore.GetValidAccessTokenAsync();

    if (string.IsNullOrEmpty(backendToken))
    {
        return new UnauthorizedObjectResult(new { message = "Backend not authenticated with cloud" });
    }

    // Use backendToken to make cloud API requests
    var result = await _cloudAuthService.GetUserInfoAsync($"Bearer {backendToken}");
    return result;
}

Cache Invalidation

When tokens are refreshed or updated, invalidate related caches:

// In Service_CloudAuth.cs
private void InvalidateUserCaches(string token)
{
    var suffix = GenerateUserKeySuffix(token);
    var keysToRemove = new[]
    {
        $"proxy:auth/user-info:{suffix}",
        $"proxy:auth/validate-token:{suffix}",
        $"proxy:sessions/sessions:{suffix}"
    };

    foreach (var key in keysToRemove)
    {
        _cache.Remove(key);
        _singleflightGates.TryRemove(key, out _);
    }
}

This is automatically called when storing a new session via StoreSession().