Conversation
Why: Enable browser-based OAuth2 authentication as an alternative to API keys, providing a more secure and user-friendly login experience that works with Rootly's magic client_id auto-provisioning. - Add internal/oauth/ package (oauth.go, tokens.go, transport.go) with: - OAuth2 config with PKCE (client_id=rootly-tui, port 19798) - Token storage consolidated in ~/.rootly-tui/config.yaml - Auto-refresh transport that persists refreshed tokens to disk - DeriveAuthBaseURL with path stripping and http:// for localhost - Redesign setup screen with auth method selector (OAuth2 default, API Key) - Add animated first-run welcome screen with ASCII logo typing effect, gradient coloring, shimmer sweep, and subtitle fade-in - First-run wizard: single Login button, auto-saves and proceeds to main screen - Returning users: full two-panel setup with Login/Save/Logout buttons - Update API client to use Bearer token via OAuth transport when available - Fix Content-Type to application/vnd.api+json for all raw HTTP requests - Auto-derive /api path for local dev OAuth endpoints - Add comprehensive tests for oauth package, config, and setup views 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
- Fix doSavePreferences wiping OAuth tokens (data loss bug) - persistingTokenSource: only save to disk when token actually changes - Pre-compute lipgloss styles for welcome animation (avoid per-char alloc) - Stop welcome animation ticks after shimmer cycle completes - Pass pre-loaded tokens to NewHTTPClientWithTokens (avoid double disk read) - Remove dead IsValidForAPI() method - Remove extra blank line (goimports) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
v0.36.0 requires Go 1.25 which CI doesn't have yet. Co-Authored-By: Claude <noreply@anthropic.com>
- Replace persistingTokenSource with retryOn401Transport that force-refreshes tokens when the server returns 401 (handles revoked/invalid tokens) - Remove hardcoded api.rootly.com → app.rootly.com mapping in DeriveAuthBaseURL; derive auth URL purely from the configured endpoint - Add debug logging to OAuth transport for troubleshooting token issues
…oked Instead of showing a cryptic token refresh error, detect ErrTokenRefreshFailed, clear stale tokens from config, and redirect the user to the setup screen with a friendly "Session expired" message.
Greptile SummaryThis PR adds OAuth2 PKCE browser-based login as the default authentication method alongside the existing API-key flow, including a new Key changes:
Minor findings (all P2, non-blocking):
Confidence Score: 5/5Safe to merge — the OAuth flow is additive, existing API-key configs continue to work unchanged, and all findings are non-blocking P2 style/polish items. All four findings are P2 (documentation inconsistency, minor code duplication, misleading comment, missing HTTP timeout on a short-lived localhost server). No correctness, data-integrity, or security defects were found. Test coverage is good with 774 tests passing. internal/app/app.go — handleOAuthExpired should reuse ClearOAuthTokens() to avoid leaving a stale OAuthExpiresAt in the config file. Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant TUI as rootly-tui
participant Browser
participant Callback as Local Callback :19798
participant AuthSrv as Rootly Auth Server
participant Config as config.yaml
User->>TUI: Press Enter on Login button
TUI->>TUI: Generate state and code verifier
TUI->>Callback: Start HTTP server on :19798
TUI->>Browser: Open authorization URL
Browser->>AuthSrv: GET /oauth/authorize
AuthSrv-->>Browser: Redirect to localhost:19798/callback
Browser->>Callback: GET /callback with code and state params
Callback->>Callback: Validate state for CSRF protection
Callback-->>Browser: HTML success page
Callback->>TUI: Send auth code via channel
TUI->>AuthSrv: POST /oauth/token to exchange code
AuthSrv-->>TUI: Access and refresh credentials
TUI->>Config: Persist credentials and endpoint
TUI->>User: Navigate to main screen
Note over TUI,AuthSrv: On subsequent API calls
TUI->>AuthSrv: Request with Bearer header
alt Server returns 401
AuthSrv-->>TUI: 401 Unauthorized
TUI->>AuthSrv: POST /oauth/token refresh grant
AuthSrv-->>TUI: New credentials
TUI->>Config: Persist refreshed credentials
TUI->>AuthSrv: Retry with new Bearer header
end
|
| - Raw HTTP requests now use `application/vnd.api+json` content type (was `application/json`) | ||
| - Forward `WindowSizeMsg` to setup screen for proper centering | ||
|
|
||
| ### Dependencies |
| case views.OAuthLoginResultMsg: | ||
| var cmd tea.Cmd | ||
| m.setup, cmd = m.setup.Update(msg) | ||
| return m, cmd | ||
|
|
||
| case views.OAuthLogoutResultMsg: | ||
| m.setup, _ = m.setup.Update(msg) |
There was a problem hiding this comment.
handleOAuthExpired doesn't clear OAuthExpiresAt and duplicates ClearOAuthTokens
The manual field-clearing here omits OAuthExpiresAt, leaving a stale timestamp in the saved config file. The Config.ClearOAuthTokens() method (added in this very PR) handles all five fields correctly and should be used here instead:
if m.cfg != nil {
m.cfg.ClearOAuthTokens()
_ = config.Save(m.cfg)
}This also avoids the duplication between the two clearing paths.
- CHANGELOG: correct oauth2 version to v0.35.0 (matches go.mod) - handleOAuthExpired: use ClearOAuthTokens() to clear all 5 fields - isLocalHost: fix misleading comment to match actual behavior - OAuth callback server: add ReadHeaderTimeout for safety
The Rootly monolith no longer supports magic client IDs. On first login, the TUI now registers dynamically via POST /oauth/register, caches the client_id in config, and reuses it on subsequent logins. Stale client IDs are cleared on invalid_client errors so the next attempt re-registers.
This comment has been minimized.
This comment has been minimized.
- Parse scope from POST /oauth/register response and cache in config - Use server-provided scopes for authorize URL instead of hardcoded list - Split URL derivation: DeriveAPIBaseURL for server-to-server calls (POST /oauth/register), DeriveAuthBaseURL strips api. prefix for browser-facing URLs (api.rootly.com → rootly.com) - Localhost and non-api. hosts (staging.rootly.com) unchanged
Why
Enable browser-based OAuth2 authentication as an alternative to API keys. This provides a more secure and user-friendly login experience using Rootly's magic
client_id=rootly-tuiauto-provisioning, matching the flow used byrootly-cli.What
New
internal/oauth/packageclient_id=rootly-tui, callback port 19798), state generation, code exchange,DeriveAuthBaseURL(strips paths, useshttp://for localhost)~/.rootly-tui/config.yaml(no separate tokens file)persistingTokenSource(saves refreshed tokens to disk) anduserAgentTransportSetup screen redesign
:19798→ PKCE exchange → stores tokenss): full two-panel layout with Login/Save/Logout + PreferencesAPI client changes
Authorization: Bearer <access_token>via OAuth transport whenuse_oauth: true/apipath for local dev OAuth endpointsContent-Typetoapplication/vnd.api+jsonfor all raw HTTP requestsConfig changes
use_oauth,oauth_access_token,oauth_refresh_token,oauth_token_type,oauth_expires_atfieldsIsValid()accepts OAuth (no API key required whenuse_oauth: true)Tests
Rollback/Revert Plan
Revert the commit. OAuth is additive — existing API key configs continue to work unchanged.
Demo/Screenshots