> ## Documentation Index
> Fetch the complete documentation index at: https://dev-doc.dualmindlab.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication Flow

> JWT validation and user synchronization internals

## JWT Middleware Pipeline

### Middleware Execution Order

```
1. Request Logging Middleware
   ↓
2. Exception Handler Middleware
   ↓
3. CORS Middleware
   ↓
4. Authentication Middleware ← JWT validation happens here
   ↓
5. Authorization Middleware ← [Authorize] attribute enforced
   ↓
6. Controller Endpoint
```

### JWT Validation Steps

**Step 1: Token Extraction**

```csharp theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
string authHeader = context.Request.Headers["Authorization"];
if (!authHeader.StartsWith("Bearer "))
    return 401 Unauthorized;

string token = authHeader.Substring(7); // Remove "Bearer " prefix
```

**Step 2: Signature Validation**

```csharp theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters {
    ValidateIssuerSigningKey = true,
    IssuerSigningKey = new SymmetricSecurityKey(
        Encoding.UTF8.GetBytes(Configuration["JWT_SECRET"])
    ),
    ValidateIssuer = true,
    ValidIssuer = $"{Configuration["SUPABASE_URL"]}/auth/v1",
    ValidateAudience = true,
    ValidAudience = "authenticated",
    ValidateLifetime = true,
    ClockSkew = TimeSpan.Zero
};

ClaimsPrincipal principal = tokenHandler.ValidateToken(
    token, 
    validationParameters, 
    out SecurityToken validatedToken
);
```

**Validation Failures**:

* Invalid signature → `SecurityTokenInvalidSignatureException` → 401
* Wrong issuer → `SecurityTokenInvalidIssuerException` → 401
* Wrong audience → `SecurityTokenInvalidAudienceException` → 401
* Expired token → `SecurityTokenExpiredException` → 401

**Step 3: Claims Extraction**

```csharp theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
var userId = principal.FindFirst("sub")?.Value;
var email = principal.FindFirst("email")?.Value;
var fullName = principal.FindFirst("full_name")?.Value;

if (string.IsNullOrEmpty(userId))
    return 401 Unauthorized;
```

**Step 4: Context Population**

```csharp theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
context.User = principal;
context.Items["UserId"] = userId;
context.Items["Email"] = email;
```

Controller accesses via:

```csharp theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
var userId = User.FindFirst("sub")?.Value;
var email = User.FindFirst("email")?.Value;
```

## User Synchronization

### Automatic Sync Trigger

Every authenticated controller action calls:

```csharp theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
[Authorize]
public async Task<IActionResult> Chat([FromBody] ChatRequest request)
{
    var userId = User.FindFirst("sub")?.Value;
    var email = User.FindFirst("email")?.Value;
    var fullName = User.FindFirst("full_name")?.Value;
    
    // User sync happens here (every request)
    await _userSyncService.EnsureUserExistsAsync(userId, email, fullName);
    
    // ... rest of logic
}
```

### EnsureUserExistsAsync Implementation

```csharp theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
public async Task EnsureUserExistsAsync(string userId, string email, string fullName)
{
    try {
        var user = new User {
            UserId = Guid.Parse(userId),
            Email = email ?? $"user_{userId}@placeholder.com",
            FullName = fullName ?? email?.Split('@')[0] ?? "User",
            Role = "user"
        };
        
        await _supabaseClient
            .From<User>()
            .Upsert(user);
            
    } catch (Exception ex) {
        _logger.LogWarning($"User sync failed for {userId}: {ex.Message}");
        // Don't throw - user sync failure shouldn't block request
    }
}
```

**SQL Executed**:

```sql theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
INSERT INTO users (user_id, email, full_name, role, created_at)
VALUES ($1, $2, $3, 'user', NOW())
ON CONFLICT (user_id) DO UPDATE
SET email = EXCLUDED.email,
    full_name = EXCLUDED.full_name,
    updated_at = NOW();
```

### Sync Characteristics

**Performance**: \~5-15ms (indexed UPSERT)

**Idempotency**: Safe to call multiple times

**Failure Handling**: Logged as warning, request continues

**Timing**: Synchronous (awaited before proceeding)

**Frequency**: Every authenticated request

## Supabase Integration

### JWT Issuer Validation

Supabase issues JWTs with specific issuer format:

```
iss: "https://<PROJECT_REF>.supabase.co/auth/v1"
```

Backend validates:

```csharp theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ValidIssuer = $"{Configuration["SUPABASE_URL"]}/auth/v1"
```

**Configuration**:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
SUPABASE_URL=https://abcdefgh.supabase.co
```

Validation ensures token came from correct Supabase project.

### Service Role vs Anon Key

**Client-Side (Supabase Anon Key)**:

```javascript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const supabase = createClient(
  SUPABASE_URL,
  SUPABASE_ANON_KEY  // Limited permissions, RLS enforced
);
```

**Server-Side (Supabase Service Role)**:

```csharp theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
var supabase = new SupabaseClient(
  Configuration["SUPABASE_URL"],
  Configuration["SUPABASE_SERVICE_ROLE_KEY"]  // Full access, bypasses RLS
);
```

**Why Service Role in Backend?**

* Full database access (no RLS restrictions)
* Perform UPSERT operations
* Read all tables regardless of RLS policies
* Backend implements authorization in application code

## Authorization vs Authentication

### Authentication

**Question**: "Who are you?"

**Mechanism**: JWT validation via middleware

**Result**: `User` principal populated with claims

**Happens**: Before controller method execution

### Authorization

**Question**: "Are you allowed to do this?"

**Mechanism**: Custom logic in controller

**Example**:

```csharp theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
var thread = await _threadsService.GetByIdAsync(threadId);
var userId = User.FindFirst("sub")?.Value;

if (thread.UserId != userId)
    return Forbid(); // 403 Forbidden
```

**Happens**: Inside controller method logic

### \[Authorize] Attribute

```csharp theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
[Authorize]  // Requires authentication but not specific authorization
public async Task<IActionResult> GetThread(string id)
{
    var thread = await _threadsService.GetByIdAsync(id);
    
    // Custom authorization check
    if (thread.Visibility == "private") {
        var userId = User.FindFirst("sub")?.Value;
        if (thread.UserId != userId)
            return Forbid();
    }
    
    return Ok(thread);
}
```

### \[AllowAnonymous] Attribute

```csharp theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
[AllowAnonymous]  // No JWT required
public IActionResult Health()
{
    return Ok(new { status = "healthy" });
}
```

**Public Endpoints**:

* `/health`
* `/api/health`
* `/api/ping`
* `/api/settings/feature-flag/{key}`
* `/api/speech/generate`

## Token Lifecycle

### Token Acquisition (Client)

```javascript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const { data } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'password'
});

const jwt = data.session.access_token;        // 1-hour expiry
const refreshToken = data.session.refresh_token; // 30-day expiry
```

### Token Usage

```javascript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
fetch('/api/arena/chat', {
  headers: {
    'Authorization': `Bearer ${jwt}`
  }
});
```

### Token Expiration

**Access Token**: Expires after 1 hour

**Detection**:

```javascript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const payload = JSON.parse(atob(jwt.split('.')[1]));
if (payload.exp * 1000 < Date.now()) {
  // Token expired
}
```

### Token Refresh

```javascript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const { data, error } = await supabase.auth.refreshSession({
  refresh_token: refreshToken
});

if (!error) {
  const newJwt = data.session.access_token;
  // Use newJwt for subsequent requests
}
```

**Automatic Refresh**: Supabase client libraries auto-refresh before expiration

## Security Considerations

### JWT Secret Protection

**Environment Variable**:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
JWT_SECRET=your_supabase_jwt_secret_here
```

**Acquisition**: From Supabase project settings → API → JWT Settings

**Critical**: Never commit to version control

**Development**: If missing, signature validation may be skipped (logged warning)

**Production**: MUST be set or all requests will fail 401

### HTTPS Requirement

JWTs transmitted in `Authorization` header (plaintext over HTTP).

**Development**: HTTP acceptable (localhost)

**Production**: HTTPS mandatory

**Why**: Bearer tokens are susceptible to man-in-the-middle attacks over HTTP

### Token Revocation

**Limitation**: JWTs cannot be revoked before expiration

**Mitigation**: Short expiry (1 hour)

**Workaround**: Track revoked tokens in database (not currently implemented)

### Supabase Auth Events

**Not Used**: DualMind doesn't listen to Supabase auth webhooks

**User Deletion**: Deleting user in Supabase doesn't cascade to DualMind database

**Implication**: Orphaned user records possible if user deleted from Supabase Auth

**Future**: Implement webhook listener for `user.deleted` event

## Development vs Production

### Development Mode

**JWT\_SECRET**: May be missing (validation skipped with warning)

**CORS**: Allows all origins

**HTTPS**: Not enforced

**Logging**: Verbose request/response logging

### Production Mode

**JWT\_SECRET**: MUST be set (validation enforced)

**CORS**: Configured allowed origins only

**HTTPS**: Enforced at infrastructure level

**Logging**: Error-level only (no verbose logging)

## Troubleshooting

### 401 Unauthorized

**Causes**:

1. Missing `Authorization` header
2. Invalid JWT signature (wrong `JWT_SECRET`)
3. Expired token (`exp` \< now)
4. Wrong issuer (Supabase URL mismatch)
5. Wrong audience (`aud != "authenticated"`)

**Debug**:

```javascript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
// Decode JWT to inspect claims (don't validate signature)
const payload = JSON.parse(atob(jwt.split('.')[1]));
console.log(payload);
```

### 403 Forbidden

**Causes**:

1. Valid JWT but accessing resource owned by different user
2. Private thread accessed by non-owner

**Debug**: Check `thread.user_id` vs JWT `sub` claim

### User Sync Failures

**Symptom**: Warnings in logs but requests succeed

**Causes**:

1. Database connectivity issue
2. Invalid user\_id format (not UUID)
3. Supabase service role key incorrect

**Impact**: Minimal (user record may be stale but auth works)

## Next Steps

<CardGroup cols="2">
  <Card title="Request Lifecycle" icon="diagram-project" href="/architecture/request-lifecycle">
    Where JWT validation fits in execution flow
  </Card>

  <Card title="System Invariants" icon="shield-check" href="/architecture/invariants">
    Authentication-related invariants
  </Card>
</CardGroup>
