diff --git a/deploy/upcloud/configs/cloudinit.sh.tmpl b/deploy/upcloud/configs/cloudinit.sh.tmpl index 4b571db..89f4565 100644 --- a/deploy/upcloud/configs/cloudinit.sh.tmpl +++ b/deploy/upcloud/configs/cloudinit.sh.tmpl @@ -17,7 +17,8 @@ done # System packages export DEBIAN_FRONTEND=noninteractive apt-get update && apt-get upgrade -y -apt-get install -y git gcc make curl libsqlite3-dev nodejs npm htop +apt-get install -y git gcc make curl libsqlite3-dev nodejs npm htop systemd-timesyncd +timedatectl set-ntp true # Swap (for small instances) if [ ! -f /swapfile ]; then diff --git a/pkg/appview/handlers/oauth_errors.go b/pkg/appview/handlers/oauth_errors.go index 8aa3fae..3523965 100644 --- a/pkg/appview/handlers/oauth_errors.go +++ b/pkg/appview/handlers/oauth_errors.go @@ -2,27 +2,46 @@ package handlers import ( "context" + "errors" "log/slog" "strings" "atcr.io/pkg/auth/oauth" + "github.com/bluesky-social/indigo/atproto/atclient" + "github.com/bluesky-social/indigo/xrpc" ) // isOAuthError checks if an error indicates OAuth authentication failure // These errors indicate the OAuth session is invalid and should be cleaned up +// Uses structured error types to avoid false positives from substring matching func isOAuthError(err error) bool { if err == nil { return false } + + // Check structured error types first + var xrpcErr *xrpc.Error + if errors.As(err, &xrpcErr) && (xrpcErr.StatusCode == 401 || xrpcErr.StatusCode == 403) { + return true + } + var apiErr *atclient.APIError + if errors.As(err, &apiErr) { + if apiErr.StatusCode == 401 || apiErr.StatusCode == 403 { + return true + } + if apiErr.Name == "InvalidToken" || apiErr.Name == "InsufficientScope" || apiErr.Name == "InvalidGrant" { + return true + } + } + + // Fallback: check for known auth-specific error strings that won't + // appear in digests or URIs errStr := strings.ToLower(err.Error()) - return strings.Contains(errStr, "401") || - strings.Contains(errStr, "403") || - strings.Contains(errStr, "invalid_token") || + return strings.Contains(errStr, "invalid_token") || strings.Contains(errStr, "invalid_grant") || strings.Contains(errStr, "use_dpop_nonce") || - strings.Contains(errStr, "unauthorized") || - strings.Contains(errStr, "token") && strings.Contains(errStr, "expired") || - strings.Contains(errStr, "authentication failed") + strings.Contains(errStr, "authentication failed") || + strings.Contains(errStr, "token expired") } // handleOAuthError checks if an error is OAuth-related and invalidates UI sessions if so diff --git a/pkg/auth/oauth/client.go b/pkg/auth/oauth/client.go index ce97085..9985b84 100644 --- a/pkg/auth/oauth/client.go +++ b/pkg/auth/oauth/client.go @@ -6,6 +6,7 @@ package oauth import ( "context" + "errors" "fmt" "log/slog" "strings" @@ -13,9 +14,11 @@ import ( "time" "atcr.io/pkg/atproto" + "github.com/bluesky-social/indigo/atproto/atclient" "github.com/bluesky-social/indigo/atproto/atcrypto" "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/xrpc" ) // permissionSetExpansions maps lexicon IDs to their expanded scope format. @@ -312,16 +315,34 @@ func (r *Refresher) DoWithSession(ctx context.Context, did string, fn func(sessi } // isAuthError checks if an error looks like an OAuth/auth failure +// Uses structured error types to avoid false positives from substring matching +// (e.g., a digest hash containing "401" in a RecordNotFound error) func isAuthError(err error) bool { if err == nil { return false } + + // Check structured error types first + var xrpcErr *xrpc.Error + if errors.As(err, &xrpcErr) && xrpcErr.StatusCode == 401 { + return true + } + var apiErr *atclient.APIError + if errors.As(err, &apiErr) { + if apiErr.StatusCode == 401 { + return true + } + if apiErr.Name == "InvalidToken" || apiErr.Name == "InsufficientScope" { + return true + } + } + + // Fallback: check for known auth-specific error strings that won't + // appear in digests or URIs errStr := strings.ToLower(err.Error()) - return strings.Contains(errStr, "unauthorized") || - strings.Contains(errStr, "invalid_token") || + return strings.Contains(errStr, "invalid_token") || strings.Contains(errStr, "insufficient_scope") || - strings.Contains(errStr, "token expired") || - strings.Contains(errStr, "401") + strings.Contains(errStr, "token expired") } // resumeSession loads a session from storage