mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-02 00:06:58 +00:00
try and trace oauth failures
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user