Skip to content

Improve oauth - #15

Merged
bigspawn merged 13 commits into
mainfrom
improve-oauth
Jan 6, 2026
Merged

Improve oauth#15
bigspawn merged 13 commits into
mainfrom
improve-oauth

Conversation

@bigspawn

@bigspawn bigspawn commented Jan 6, 2026

Copy link
Copy Markdown
Owner

No description provided.

Changed error message from "path must be relative" to "path must be absolute"
to match the actual validation logic using path.IsAbs().
Changed all log.Fatalf calls in HTTP callback handlers and server
shutdown functions to log.Printf with proper return statements.
This prevents the application from terminating unexpectedly on
runtime errors.
Changes:
- shutdownServer: Added defer cancel(), replaced log.Fatalf with log.Printf+return
- Callback handler: Added code validation, replaced 3 log.Fatalf calls with log.Printf+return
- Server startup goroutine: Replaced log.Fatalf with log.Printf
Improves graceful error handling and prevents process termination
on recoverable errors.
Added sync.RWMutex protection to OAuth struct to prevent race
conditions when token is accessed concurrently from multiple
goroutines during API calls and token refresh.
Changes:
- Added tokenMu sync.RWMutex field to protect token access
- Added stateMu sync.RWMutex field for CSRF state (preparation for Issue 1)
- Added state string field with random generation in NewOAuth
- Updated GetAuthURL() to use stateMu.RLock() for state access
- Updated ExchangeToken() to use tokenMu.Lock() for token write
- Updated TokenSource() to use tokenMu.RLock() for token read
- Updated Token() to use tokenMu.Lock() for token write during refresh
- Updated NeedInit() to use tokenMu.RLock() for token read
- Updated loadTokenFromFile() to use tokenMu.Lock() for token write
Race detector confirms no data races with go test -race.
Added proper CSRF protection to OAuth callback handler by validating
the state parameter. State is now generated as a cryptographically
random value and validated in the callback.
Changes:
- State generation already added in Issue 2 (randHTTPParamString(32))
- Added state parameter validation in callback handler
- Returns HTTP 400 if state parameter is missing
- Returns HTTP 400 if state parameter doesn't match expected value
- Logs state mismatches for security monitoring
- Uses stateMu.RLock() for thread-safe state access
This prevents CSRF attacks where an attacker could trick a user
into authorizing a malicious application.
Compliant with RFC 6749 Section 4.1.1 and RFC 9700 (2025).
Fixed incorrect PKCE implementation that used "plain" method instead
of "S256" (SHA-256). The old implementation had security vulnerabilities:
- Used same value for code_challenge and code_verifier
- Used "plain" method which doesn't provide proper protection
Changes:
- Use oauth2.GenerateVerifier() for proper 32-byte random verifier
- Use oauth2.S256ChallengeOption(verifier) for S256 challenge
- Use oauth2.VerifierOption(verifier) for token exchange
- Remove unused net/url import
This follows RFC 7636 Section 4.3 which requires S256 method support.
Added proper PKCE (Proof Key for Code Exchange) with S256 method
to AniList OAuth flow. Previously AniList had no PKCE protection
at all, which is required by OAuth 2.1 and RFC 9700.
Changes:
- Generate code verifier using oauth2.GenerateVerifier()
- Add S256 challenge option for authorization URL
- Add verifier option for token exchange
- Keep existing AccessTypeOffline for refresh token support
This provides defense in depth against authorization code
interception attacks as specified in RFC 7636.
Fixed context propagation issue where Token() used context.Background()
instead of the caller's context. This prevented proper cancellation
of token refresh operations during application shutdown.
Changes:
- Changed TokenSource() to accept context parameter
- Created contextAwareTokenSource wrapper to carry context through Token() calls
- Added TokenWithContext() method that accepts context for token refresh
- Token() now delegates to TokenWithContext(context.Background()) for backward compatibility
- Updated NewMyAnimeListClient to pass context to TokenSource()
- Updated NewAnilistClient to pass context to TokenSource()
Token refresh now respects context cancellation signals from SIGINT/SIGTERM,
allowing graceful shutdown instead of hanging on token operations.
Implemented atomic write pattern using write-rename to prevent
token file corruption if the process crashes during write.
Changes:
- Create temporary file in same directory as target
- Write token data to temp file
- Sync to disk with file.Sync() to ensure data is flushed
- Atomic rename to replace target file
- Clean up temp file on error
This prevents partial writes and ensures token file is always
valid, even if process crashes during write operation.
Follows best practices for atomic file operations on Unix/Linux.
MyAnimeList OAuth was failing with "invalid_grant" error during token
exchange. Investigation revealed MAL requires plain PKCE method, not S256.

The authorization code was returned correctly, but token exchange failed
because MAL expects code_challenge to equal code_verifier (plain method),
not a SHA-256 hash (S256 method).

Changes:
- Use SetAuthURLParam("code_challenge", verifier) for plain challenge
- Use SetAuthURLParam("code_challenge_method", "plain") explicitly
- Keep VerifierOption for token exchange

AniList continues to use S256 PKCE correctly (unchanged).

Related: Reverts MAL-specific change from commit 2a26ba6 which assumed
S256 was supported by all providers.
@bigspawn bigspawn self-assigned this Jan 6, 2026
When user pressed ^C during OAuth authorization, the application
continued running instead of exiting cleanly. The server kept running
in background and could complete the callback after interrupt.

Changes:
- Use defer for guaranteed server shutdown in getToken()
- Use buffered channel to prevent deadlock
- Return server reference from startServer()
- Use req.Context() instead of parent context in callback
- Check ctx.Err() after getToken() and propagate error
- Remove shutdownServer() function (no longer needed)

This follows the pattern used in production projects like netbird,
where defer ensures cleanup on any exit path (success, cancel, error).
Added oauth_test.go with 26 tests covering all OAuth functionality:
- Basic tests (NewOAuth, NeedInit, State validation, CreateDir)
- File operations (ReadWrite, Atomic writes, Missing file)
- PKCE configuration (MAL plain method, AniList S256 method)
- CSRF state validation (missing, mismatched, valid state)
- Context cancellation (token refresh respects context)
- Thread safety (concurrent token/state access with -race)
- Mock OAuth server integration tests

Test coverage for oauth.go:
- NewOAuth: 85.7%
- GetAuthURL: 100%
- NeedInit: 100%
- TokenSource: 100%
- readTokenFile: 84.6%
- createDirIfNotExists: 80%

All tests pass with race detector enabled (go test -race).

Bug fix: createDirIfNotExists now returns nil after successfully
creating a directory (previously returned error formatting with nil).

Testing approach based on best practices:
- httptest for mock OAuth server
- t.TempDir for file isolation
- Table-driven tests following existing patterns
- sync.WaitGroup for concurrent access tests
- Remove unused constant randNumb from myanimelist.go
- Add cleanupFile() helper to properly log cleanup errors
- Add //nolint:containedctx for contextAwareTokenSource (forced by oauth2.TokenSource interface)
- Fix errcheck issues in oauth_test.go (resp.Body.Close(), w.Write())
- Fix formatting issues (gofumpt, gci)
- Fix unused parameter in test handler
- Fix staticcheck nil pointer warnings

All errors are now logged for observability, nothing is ignored.
@bigspawn
bigspawn merged commit f69d075 into main Jan 6, 2026
3 checks passed
@bigspawn
bigspawn deleted the improve-oauth branch January 6, 2026 20:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant