try and trace oauth failures

This commit is contained in:
Evan Jarrett
2025-11-09 13:07:35 -06:00
parent 15d3684cf6
commit 628f8b7c62
4 changed files with 206 additions and 3 deletions
+97
View File
@@ -337,6 +337,103 @@ func scopesMatch(stored, desired []string) bool {
return true
}
// GetSessionStats returns statistics about stored OAuth sessions
// Useful for monitoring and debugging session health
func (s *OAuthStore) GetSessionStats(ctx context.Context) (map[string]interface{}, error) {
stats := make(map[string]interface{})
// Total sessions
var totalSessions int
err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM oauth_sessions`).Scan(&totalSessions)
if err != nil {
return nil, fmt.Errorf("failed to count sessions: %w", err)
}
stats["total_sessions"] = totalSessions
// Sessions by age
var sessionsOlderThan1Hour, sessionsOlderThan1Day, sessionsOlderThan7Days int
err = s.db.QueryRowContext(ctx, `
SELECT COUNT(*) FROM oauth_sessions
WHERE updated_at < datetime('now', '-1 hour')
`).Scan(&sessionsOlderThan1Hour)
if err == nil {
stats["sessions_idle_1h+"] = sessionsOlderThan1Hour
}
err = s.db.QueryRowContext(ctx, `
SELECT COUNT(*) FROM oauth_sessions
WHERE updated_at < datetime('now', '-1 day')
`).Scan(&sessionsOlderThan1Day)
if err == nil {
stats["sessions_idle_1d+"] = sessionsOlderThan1Day
}
err = s.db.QueryRowContext(ctx, `
SELECT COUNT(*) FROM oauth_sessions
WHERE updated_at < datetime('now', '-7 days')
`).Scan(&sessionsOlderThan7Days)
if err == nil {
stats["sessions_idle_7d+"] = sessionsOlderThan7Days
}
// Recent sessions (updated in last 5 minutes)
var recentSessions int
err = s.db.QueryRowContext(ctx, `
SELECT COUNT(*) FROM oauth_sessions
WHERE updated_at > datetime('now', '-5 minutes')
`).Scan(&recentSessions)
if err == nil {
stats["sessions_active_5m"] = recentSessions
}
return stats, nil
}
// ListSessionsForMonitoring returns a list of all sessions with basic info for monitoring
// Returns: DID, session age (minutes), last update time
func (s *OAuthStore) ListSessionsForMonitoring(ctx context.Context) ([]map[string]interface{}, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT
account_did,
session_id,
created_at,
updated_at,
CAST((julianday('now') - julianday(updated_at)) * 24 * 60 AS INTEGER) as idle_minutes
FROM oauth_sessions
ORDER BY updated_at DESC
`)
if err != nil {
return nil, fmt.Errorf("failed to query sessions: %w", err)
}
defer rows.Close()
var sessions []map[string]interface{}
for rows.Next() {
var did, sessionID, createdAt, updatedAt string
var idleMinutes int
if err := rows.Scan(&did, &sessionID, &createdAt, &updatedAt, &idleMinutes); err != nil {
slog.Warn("Failed to scan session row", "error", err)
continue
}
sessions = append(sessions, map[string]interface{}{
"did": did,
"session_id": sessionID,
"created_at": createdAt,
"updated_at": updatedAt,
"idle_minutes": idleMinutes,
})
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating sessions: %w", err)
}
return sessions, nil
}
// makeSessionKey creates a composite key for session storage
func makeSessionKey(did, sessionID string) string {
return fmt.Sprintf("%s:%s", did, sessionID)
+15 -3
View File
@@ -167,9 +167,21 @@ func (nr *NamespaceResolver) Repository(ctx context.Context, name reference.Name
var err error
serviceToken, err = token.GetOrFetchServiceToken(ctx, nr.refresher, did, holdDID, pdsEndpoint)
if err != nil {
slog.Error("Failed to get service token", "component", "registry/middleware", "did", did, "error", err)
slog.Error("User needs to re-authenticate via credential helper", "component", "registry/middleware")
return nil, nr.authErrorMessage("OAuth session expired")
slog.Error("Failed to get service token",
"component", "registry/middleware",
"did", did,
"holdDID", holdDID,
"pdsEndpoint", pdsEndpoint,
"error", err)
// Check if this is likely an OAuth session expiration
errMsg := err.Error()
if strings.Contains(errMsg, "OAuth session") || strings.Contains(errMsg, "OAuth validation") {
return nil, nr.authErrorMessage("OAuth session expired or invalidated by PDS. Your session has been cleared")
}
// Generic service token error
return nil, nr.authErrorMessage(fmt.Sprintf("Failed to obtain storage credentials: %v", err))
}
}
+50
View File
@@ -231,3 +231,53 @@ func (r *Refresher) resumeSession(ctx context.Context, did string) (*oauth.Clien
}
return session, nil
}
// DeleteSession removes an OAuth session from storage and optionally invalidates the UI session
// This is called when OAuth authentication fails to force re-authentication
func (r *Refresher) DeleteSession(ctx context.Context, did string) error {
// Parse DID
accountDID, err := syntax.ParseDID(did)
if err != nil {
return fmt.Errorf("failed to parse DID: %w", err)
}
// Get the session ID before deleting (for logging)
type sessionGetter interface {
GetLatestSessionForDID(ctx context.Context, did string) (*oauth.ClientSessionData, string, error)
}
getter, ok := r.clientApp.Store.(sessionGetter)
if !ok {
return fmt.Errorf("store must implement GetLatestSessionForDID")
}
_, sessionID, err := getter.GetLatestSessionForDID(ctx, did)
if err != nil {
// No session to delete - this is fine
slog.Debug("No OAuth session to delete", "did", did)
return nil
}
// Delete OAuth session from database
if err := r.clientApp.Store.DeleteSession(ctx, accountDID, sessionID); err != nil {
slog.Warn("Failed to delete OAuth session", "did", did, "sessionID", sessionID, "error", err)
return fmt.Errorf("failed to delete OAuth session: %w", err)
}
slog.Info("Deleted stale OAuth session",
"component", "oauth/refresher",
"did", did,
"sessionID", sessionID,
"reason", "OAuth authentication failed")
// Also invalidate the UI session if store is configured
if r.uiSessionStore != nil {
r.uiSessionStore.DeleteByDID(did)
slog.Info("Invalidated UI session for DID",
"component", "oauth/refresher",
"did", did,
"reason", "OAuth session deleted")
}
return nil
}
+44
View File
@@ -48,6 +48,23 @@ func GetOrFetchServiceToken(
if err != nil {
// OAuth session unavailable - fail
InvalidateServiceToken(did, holdDID)
slog.Error("Failed to get OAuth session for service token",
"component", "token/servicetoken",
"did", did,
"holdDID", holdDID,
"pdsEndpoint", pdsEndpoint,
"error", err,
"errorType", fmt.Sprintf("%T", err))
// Delete the stale OAuth session to force re-authentication
// This also invalidates the UI session automatically
if delErr := refresher.DeleteSession(ctx, did); delErr != nil {
slog.Warn("Failed to delete stale OAuth session",
"component", "token/servicetoken",
"did", did,
"error", delErr)
}
return "", fmt.Errorf("failed to get OAuth session: %w", err)
}
@@ -74,6 +91,25 @@ func GetOrFetchServiceToken(
if err != nil {
// Auth error - may indicate expired tokens or corrupted session
InvalidateServiceToken(did, holdDID)
slog.Error("OAuth authentication failed during service token request",
"component", "token/servicetoken",
"did", did,
"holdDID", holdDID,
"pdsEndpoint", pdsEndpoint,
"url", serviceAuthURL,
"error", err,
"errorType", fmt.Sprintf("%T", err),
"hint", "This likely means the PDS rejected the OAuth session - refresh token may be expired or invalidated")
// Delete the stale OAuth session to force re-authentication
// This also invalidates the UI session automatically
if delErr := refresher.DeleteSession(ctx, did); delErr != nil {
slog.Warn("Failed to delete stale OAuth session",
"component", "token/servicetoken",
"did", did,
"error", delErr)
}
return "", fmt.Errorf("OAuth validation failed: %w", err)
}
defer resp.Body.Close()
@@ -82,6 +118,14 @@ func GetOrFetchServiceToken(
// Service auth failed
bodyBytes, _ := io.ReadAll(resp.Body)
InvalidateServiceToken(did, holdDID)
slog.Error("Service token request returned non-200 status",
"component", "token/servicetoken",
"did", did,
"holdDID", holdDID,
"pdsEndpoint", pdsEndpoint,
"statusCode", resp.StatusCode,
"responseBody", string(bodyBytes),
"hint", "PDS rejected the service token request - check PDS logs for details")
return "", fmt.Errorf("service auth failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}