Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.5 7ddd772ddc Fix missing dependency for SimpleTimedLogger in oauth_service
Add simple_timed_logger dependency that was missing after adding
diagnostic logging in PR #4974.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 05:32:44 -08:00
adminandClaude Opus 4.5 2fe92778c6 Add lobby environment and user display fields to ConnectionHandler
- Add lobbyEnvironmentText and lobbyUserText fields
- Add UpdateLobbyStatusDisplays() to populate them when entering lobby
- Shows OAuth DisplayName or classic login username

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 21:55:15 -08:00
adminandClaude Opus 4.5 bb81d58b0f Wire up lobby UI elements in Unity scene
- Connect logout button
- Connect lobbyEnvironmentText and lobbyUserText fields

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 21:51:32 -08:00
adminandClaude Opus 4.5 f896d42267 Update OAUTH_NEXT_STEPS.md with current status and remaining work
- Mark completed items (headshots, logout, lobby display, etc.)
- Add new issues discovered (intermittent expired errors, token expiry bug)
- Reorganize implementation plan into Phase 2 (remaining) and Phase 3 (nice-to-haves)
- Update status of all known issues

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 21:49:37 -08:00
4 changed files with 814 additions and 193 deletions
+107 -61
View File
@@ -4,70 +4,77 @@
The OAuth implementation is functional but has several gaps that need addressing before it's production-ready. This document outlines the known issues, proposes a comprehensive user identity model, and provides a prioritized implementation plan.
## Current State
## Current State (Updated January 2026)
### What Works
### What Works
- Discord OAuth flow (server-mediated polling)
- Google OAuth flow
- JWT token generation and validation
- User creation and display name setting
- Auto-login with stored tokens
- Basic game creation and play with OAuth users
- Headshot fetching via public CDN (no auth required)
- Logout button in lobby (preserves tokens for quick reconnect)
- Environment (prod/qa) and user display in lobby
- Game identity with userName = displayName (PR #4964 merged)
### Known Issues
#### 1. Game Identity Model Fragility (Critical - but PR #4964 is required now)
**Problem**: PR #4964 sets `userName = displayName` for backwards compatibility. This works but creates a coupling: if a user changes their displayName later, their game history becomes orphaned.
#### 1. Game Identity Model Fragility (Deferred)
**Status**: Accepted for now. PR #4964 merged with `userName = displayName`.
**Current flow**:
**Current behavior**:
- Games store `userNameToFactionId: Map[String, Int]`
- For JWT users, this maps displayName → factionId
- displayName is technically mutable (users could change it)
- No migration path when displayName changes
**Recommendation**: **Merge PR #4964 now.** It fixes the immediate crash (admin server NPE, games with null players). The fragility is acceptable because:
**Why this is acceptable**:
1. We don't currently have a "change display name" feature
2. The alternative (using userId) requires more extensive changes
3. We can migrate to userId-based identity in Phase 2
3. Can migrate to userId-based identity later if needed
**Phase 2 fix**: Change `userName = userId` instead of `displayName`, add display name resolution layer for UI.
#### 2. In-Game Headshot Fetching ✅ FIXED
**Solution**: Made the `eagle0-headshots` S3 bucket public and enabled CDN.
- Client now fetches directly from `https://eagle0-headshots.sfo3.cdn.digitaloceanspaces.com/`
- No authentication required
- Works for both OAuth and Basic Auth users
- Simpler architecture, no dependency on home Mac server
#### 2. In-Game Headshot Fetching Broken (High)
**Problem**: Hero/character portrait images can't be fetched by OAuth users.
#### 3. Logout from Lobby ✅ FIXED
**Solution**: Added logout button to lobby UI (PR #4967).
- Button disconnects from server and returns to connection screen
- Intentionally does NOT clear OAuth tokens
- Allows quick reconnect with same account without full OAuth flow
**Architecture** (discovered via investigation):
- Headshots are AI-generated character portraits stored in S3 bucket `eagle0-headshots`
- Client fetches from `https://eagle0.net/assets/headshots/{path}` (HOME MAC, not prod!)
- The nginx on `eagle0.net` validates auth and returns 302 redirect to S3 signed URL
- Client follows redirect to S3 (no auth needed for signed URLs)
**The Problem**:
- Game gRPC connects to `prod.eagle0.net` (DigitalOcean)
- Headshot fetching goes to `eagle0.net` (home Mac) - **different server!**
- The `eagle0.net` nginx likely only validates Basic Auth, not JWT Bearer tokens
- Client code (ConnectionHandler.cs:579-581) correctly sets Bearer token, but server rejects it
**Why this worked before**: Basic Auth users had credentials for both game (prod.eagle0.net) and headshots (eagle0.net).
**Why it breaks with OAuth**: OAuth users have JWT for prod.eagle0.net but the eagle0.net headshot endpoint doesn't know how to validate JWTs.
**Note**: This is NOT about user avatars from Discord/Google. Those are a separate future feature.
#### 3. No Logout from Lobby (Medium)
**Problem**: Once auto-logged in via OAuth, users have no way to log out and return to the connection screen to use a different account or auth method.
**Required**: A logout button in the lobby UI that clears tokens and returns to connection screen.
#### 4. Display Name Uniqueness Not Enforced (Medium)
#### 4. Display Name Uniqueness Not Enforced (Medium) - OPEN
**Problem**: User was able to set displayName "nolen" when that name was already taken.
**Root cause**: Either the uniqueness check is buggy, or the displayNameIndex wasn't populated correctly during user creation.
**Root cause**: Unknown - needs investigation. Either:
- The uniqueness check is buggy
- The displayNameIndex wasn't populated correctly during user creation
- Race condition during concurrent registrations
#### 5. Admin Server Crashes (Fixed)
**Problem**: `GetRunningGames` threw NPE because game player info had null strings.
#### 5. Admin Server Crashes ✅ FIXED
**Solution**: PR #4964 sets `userName = displayName` for JWT users.
**Root cause**: JWT users had null `userName`, causing games to be created with null player identities.
#### 6. Intermittent "Expired" Errors During Login (Medium) - INVESTIGATING
**Problem**: Users occasionally get "OAuth session expired" errors even when server logs show the callback succeeded.
**Fix**: PR #4964 sets `userName = displayName` for JWT users.
**Status**: Added diagnostic logging in PR #4974 to trace:
- State creation in `getAuthUrl`
- State lookup in `handleCallback`
- Result lookup in `checkStatus`
**Possible causes**:
- State mismatch between client and server
- Race condition in polling
- Cleanup running at wrong time
#### 7. Token Expiry Field Bug ✅ FIXED
**Problem**: `CheckOAuthStatusResponse.expiresAt` was returning refresh token expiry (30 days) instead of access token expiry (7 days).
**Solution**: Fixed in PR #4974 to calculate correct access token expiry.
---
@@ -163,42 +170,81 @@ When a user logs in with a new OAuth provider:
## Implementation Plan
### Phase 1: Stabilization (Immediate)
### Phase 1: Stabilization ✅ COMPLETE
#### 1.1 Fix Display Name Uniqueness Bug
#### 1.1 Fix Display Name Uniqueness Bug - OPEN
- [ ] Investigate why "nolen" was allowed when it existed
- [ ] Add logging to `setDisplayName` to trace the issue
- [ ] Ensure `displayNameIndex` is correctly maintained
- [ ] Add unit tests for uniqueness enforcement
#### 1.2 Add Logout Button to Lobby
- [ ] Add "Logout" button to lobby UI (near connection status)
- [ ] On click: call `OAuthManager.LogoutAsync()`
- [ ] Clear `TokenStorage`
- [ ] Disconnect from server
- [ ] Navigate to connection screen
#### 1.2 Add Logout Button to Lobby ✅ DONE
- [x] Add "Logout" button to lobby UI
- [x] Disconnect from server
- [x] Navigate to connection screen
- [x] Preserve OAuth tokens for quick reconnect (intentional change from original plan)
#### 1.3 Merge PR #4964 (userName = displayName)
- [ ] This is a stopgap - games will break if displayName changes
- [ ] Document this limitation
- [ ] Plan Phase 2 migration to userId-based identity
#### 1.3 Merge PR #4964 (userName = displayName) ✅ DONE
- [x] Merged - games work with OAuth users
- [x] Documented limitation (games break if displayName changes)
### Phase 2: Stable Identity (Next Sprint)
#### 1.4 Fix Headshot Fetching ✅ DONE
- [x] Made eagle0-headshots bucket public
- [x] Client fetches directly from CDN
- [x] No authentication required
#### 2.1 Migrate to userId-based Game Identity
#### 1.5 Add Lobby Status Display ✅ DONE
- [x] Show environment (prod/qa) in lobby
- [x] Show current user in lobby (OAuth displayName or classic username)
### Phase 2: Remaining Work (Priority Order)
#### 2.1 Diagnose Intermittent "Expired" Errors - IN PROGRESS
- [x] Add diagnostic logging (PR #4974)
- [ ] Deploy and reproduce the issue
- [ ] Analyze logs to identify root cause
- [ ] Implement fix based on findings
#### 2.2 Fix Display Name Uniqueness
- [ ] Investigate UserService.setDisplayName logic
- [ ] Check displayNameIndex population
- [ ] Add logging to trace the issue
- [ ] Fix the bug and add tests
#### 2.3 Wire Up Lobby UI in Unity
- [ ] Connect `lobbyEnvironmentText` to TextMeshProUGUI in scene
- [ ] Connect `lobbyUserText` to TextMeshProUGUI in scene
#### 2.4 Implement Token Refresh During Gameplay
- [ ] Implement `RefreshToken` RPC on server (currently throws UNIMPLEMENTED)
- [ ] Store refresh tokens server-side for validation
- [ ] Add proactive refresh in client before token expires
- [ ] Handle refresh during reconnection attempts
### Phase 3: Nice-to-Haves (Future)
#### 3.1 Proactive Token Refresh
- [ ] Monitor token expiry in client
- [ ] Refresh automatically when < 5 minutes remaining
- [ ] Update TokenStorage with new access token
#### 3.2 Better Error Messages
- [ ] Distinguish between network errors and auth errors
- [ ] Show user-friendly messages for OAuth failures
- [ ] Add retry suggestions
#### 3.3 Session Persistence Across Server Restarts
- [ ] Move pendingOAuth from in-memory TrieMap to Redis/database
- [ ] Move completedOAuth to Redis with TTL
- [ ] Server can restart without breaking in-flight OAuth flows
#### 3.4 Migrate to userId-based Game Identity (Deferred)
- [ ] Change `AuthorizationUtils.userName` to return `userId` for JWT users
- [ ] Add `UserService.resolveDisplayName(id: String): String` for UI display
- [ ] Update game UI to resolve userIds to displayNames
- [ ] Existing Basic Auth games continue to work (userName is literal)
#### 2.2 Headshot Fetching with OAuth
- [ ] Investigate current headshot fetching implementation
- [ ] Add avatar proxy endpoint: `GET /api/avatar/{userId}`
- [ ] Proxy requests to OAuth provider CDN
- [ ] Add caching layer (Redis or local file cache)
- [ ] Update client to use new endpoint for OAuth users
#### 2.3 Display Name Change Support
#### 3.5 Display Name Change Support (Requires 3.4)
- [ ] Add `ChangeDisplayName` RPC
- [ ] Validate new name is unique
- [ ] Update user record
@@ -112,6 +112,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
[Header("Lobby Controls")]
public Button logoutButton;
public TextMeshProUGUI lobbyEnvironmentText;
public TextMeshProUGUI lobbyUserText;
public GameObject runningGamesListArea;
public GameObject runningGamesListItemPrefab;
@@ -270,6 +272,22 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
if (logoutButton != null) { logoutButton.onClick.AddListener(OnLogoutClicked); }
}
private void UpdateLobbyStatusDisplays() {
// Show environment (prod/qa)
if (lobbyEnvironmentText != null) {
lobbyEnvironmentText.text = ConnectedEnvironmentName ?? "";
}
// Show current user - prefer OAuth display name, fall back to classic login name
if (lobbyUserText != null) {
if (_useOAuth && !string.IsNullOrEmpty(TokenStorage.DisplayName)) {
lobbyUserText.text = TokenStorage.DisplayName;
} else {
lobbyUserText.text = nameField.text;
}
}
}
private async void OnLogoutClicked() {
Debug.Log("[ConnectionHandler] Logout button clicked");
@@ -523,6 +541,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
connectionPanel.gameObject.SetActive(false);
gameSelectionPanel.gameObject.SetActive(true);
// Update lobby status displays
UpdateLobbyStatusDisplays();
// Set up running games table
foreach (Transform row in runningGamesListArea.transform) { Destroy(row.gameObject); }
foreach (var runningGame in lobbyResponse.RunningGames) {
File diff suppressed because it is too large Load Diff
@@ -34,6 +34,7 @@ scala_library(
deps = [
":oauth_config",
"//src/main/protobuf/net/eagle0/eagle/api:auth_scala_grpc",
"//src/main/scala/net/eagle0/common:simple_timed_logger",
"@maven//:org_json4s_json4s_ast_3",
"@maven//:org_json4s_json4s_core_3",
"@maven//:org_json4s_json4s_native_3",