fix security issue with refresh tokens. on logout force a full re-auth

This commit is contained in:
Evan Jarrett
2025-10-12 13:48:50 -05:00
parent 4ab3cea3d1
commit 6950346f7c
4 changed files with 10 additions and 111 deletions
+1 -5
View File
@@ -469,11 +469,7 @@ func initializeUIRoutes(database *sql.DB, readOnlyDB *sql.DB, sessionStore *db.S
Templates: templates,
}).Methods("GET")
router.Handle("/auth/oauth/login", &uihandlers.LoginSubmitHandler{
Refresher: refresher,
Directory: oauthApp.Directory(),
SessionStore: sessionStore,
}).Methods("POST")
router.Handle("/auth/oauth/login", &uihandlers.LoginSubmitHandler{}).Methods("POST")
// Public routes (with optional auth for navbar)
// SECURITY: Public pages use read-only DB
-95
View File
@@ -1,14 +1,8 @@
package handlers
import (
"fmt"
"html/template"
"net/http"
"time"
"atcr.io/pkg/auth/oauth"
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/atproto/syntax"
)
// LoginHandler shows the OAuth login form
@@ -38,14 +32,6 @@ func (h *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// LoginSubmitHandler processes the login form submission
type LoginSubmitHandler struct {
Refresher *oauth.Refresher
Directory identity.Directory
SessionStore UISessionStore
}
// UISessionStore is the interface for UI session management
type UISessionStore interface {
CreateWithOAuth(did, handle, pdsEndpoint, oauthSessionID string, duration time.Duration) (string, error)
}
func (h *LoginSubmitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -65,71 +51,6 @@ func (h *LoginSubmitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// Attempt silent login first
if h.Refresher != nil && h.Directory != nil && h.SessionStore != nil {
// Parse handle
handleSyntax, err := syntax.ParseHandle(handle)
if err == nil {
// Resolve handle to identity (DID + PDS endpoint)
ident, err := h.Directory.LookupHandle(r.Context(), handleSyntax)
if err == nil {
did := ident.DID.String()
// Try to get existing OAuth session
session, err := h.Refresher.GetSession(r.Context(), did)
if err == nil {
// Check if the session has all required scopes
requiredScopes := oauth.GetDefaultScopes()
sessionScopes := session.Data.Scopes
if !hasAllScopes(sessionScopes, requiredScopes) {
fmt.Printf("DEBUG [auth]: Session scopes mismatch for %s. Required: %v, Have: %v. Forcing re-auth.\n",
handle, requiredScopes, sessionScopes)
} else {
// Found valid OAuth session with all required scopes! Create UI session silently
fmt.Printf("DEBUG [auth]: Silent login successful for %s (DID: %s)\n", handle, did)
// Get PDS endpoint from identity
pdsEndpoint := ident.PDSEndpoint()
// Get OAuth sessionID from refresher
sessionID := h.Refresher.GetSessionID(did)
uiSessionID, err := h.SessionStore.CreateWithOAuth(did, handle, pdsEndpoint, sessionID, 30*24*time.Hour)
if err == nil {
// Set session cookie
http.SetCookie(w, &http.Cookie{
Name: "atcr_session",
Value: uiSessionID,
Path: "/",
MaxAge: 30 * 86400, // 30 days
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
// Redirect to return URL
fmt.Printf("DEBUG [auth]: Silent login complete, redirecting to %s\n", returnTo)
http.Redirect(w, r, returnTo, http.StatusFound)
return
}
fmt.Printf("WARNING [auth]: Failed to create UI session during silent login: %v\n", err)
}
} else {
fmt.Printf("DEBUG [auth]: No valid OAuth session found for %s: %v\n", handle, err)
}
} else {
fmt.Printf("DEBUG [auth]: Failed to resolve handle %s: %v\n", handle, err)
}
} else {
fmt.Printf("DEBUG [auth]: Failed to parse handle %s: %v\n", handle, err)
}
}
// Silent login failed or not configured - proceed with full OAuth flow
fmt.Printf("DEBUG [auth]: Proceeding with full OAuth flow for %s\n", handle)
// Store return_to in cookie so callback can use it
http.SetCookie(w, &http.Cookie{
Name: "oauth_return_to",
@@ -144,19 +65,3 @@ func (h *LoginSubmitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Redirect to OAuth authorize with handle
http.Redirect(w, r, "/auth/oauth/authorize?handle="+handle, http.StatusFound)
}
// hasAllScopes checks if grantedScopes contains all requiredScopes
func hasAllScopes(grantedScopes, requiredScopes []string) bool {
grantedSet := make(map[string]bool)
for _, scope := range grantedScopes {
grantedSet[scope] = true
}
for _, required := range requiredScopes {
if !grantedSet[required] {
return false
}
}
return true
}
+4 -9
View File
@@ -11,7 +11,7 @@ const ProfileRKey = "self"
// EnsureProfile checks if a user's profile exists and creates it if needed
// This should be called during authentication (OAuth exchange or token service)
// If defaultHoldEndpoint is provided and profile doesn't exist, creates profile with that default
// If defaultHoldEndpoint is provided, creates profile with that default (or empty if not provided)
func EnsureProfile(ctx context.Context, client *Client, defaultHoldEndpoint string) error {
// Check if profile already exists
profile, err := client.GetRecord(ctx, SailorProfileCollection, ProfileRKey)
@@ -20,14 +20,8 @@ func EnsureProfile(ctx context.Context, client *Client, defaultHoldEndpoint stri
return nil
}
// Profile doesn't exist
// Only create if we have a default hold endpoint to set
if defaultHoldEndpoint == "" {
// No default configured, don't create empty profile
return nil
}
// Create new profile with default hold
// Profile doesn't exist - create it
// defaultHoldEndpoint can be empty string (user will need to configure it later)
newProfile := NewSailorProfileRecord(defaultHoldEndpoint)
_, err = client.PutRecord(ctx, SailorProfileCollection, ProfileRKey, newProfile)
@@ -35,6 +29,7 @@ func EnsureProfile(ctx context.Context, client *Client, defaultHoldEndpoint stri
return fmt.Errorf("failed to create sailor profile: %w", err)
}
fmt.Printf("DEBUG [profile]: Created sailor profile with defaultHold=%s\n", defaultHoldEndpoint)
return nil
}
+5 -2
View File
@@ -267,10 +267,13 @@ func (s *Server) fetchAndStoreAvatar(ctx context.Context, did, sessionID, handle
// Create authenticated atproto client using the indigo session's API client
client := atproto.NewClientWithIndigoClient(pdsEndpoint, did, session.APIClient())
// Ensure sailor profile exists (creates with default hold on first login)
// Ensure sailor profile exists (creates with default hold if configured, or empty profile if not)
fmt.Printf("DEBUG [oauth/server]: Ensuring profile exists for %s (defaultHold=%s)\n", did, s.defaultHoldEndpoint)
if err := atproto.EnsureProfile(ctx, client, s.defaultHoldEndpoint); err != nil {
fmt.Printf("WARNING [oauth/server]: Failed to ensure profile for %s: %v\n", did, err)
// Continue anyway - profile creation is not critical
// Continue anyway - profile creation is not critical for avatar fetch
} else {
fmt.Printf("DEBUG [oauth/server]: Profile ensured for %s\n", did)
}
// Fetch user's profile record from PDS (contains blob references)