mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 04:06:58 +00:00
fix security issue with refresh tokens. on logout force a full re-auth
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user