diff --git a/db.go b/db.go index b3a2993..f0ea145 100644 --- a/db.go +++ b/db.go @@ -111,8 +111,11 @@ CREATE TABLE IF NOT EXISTS oauth_sessions ( token_type TEXT NOT NULL DEFAULT 'DPoP', expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW(), - dpop_nonce TEXT, dpop_private_jwk TEXT, + dpop_authserver_nonce TEXT, + dpop_pds_nonce TEXT, + pds_url TEXT, + authserver_iss TEXT, token_expiry TIMESTAMP ); @@ -227,6 +230,13 @@ func OpenDatabase(connString string) (*DB, error) { pool.Exec(ctx, "ALTER TABLE oauth_sessions ADD COLUMN IF NOT EXISTS token_expiry TIMESTAMP") // Make access_token nullable (session created before tokens obtained) pool.Exec(ctx, "ALTER TABLE oauth_sessions ALTER COLUMN access_token DROP NOT NULL") + // Add missing OAuth session columns + pool.Exec(ctx, "ALTER TABLE oauth_sessions ADD COLUMN IF NOT EXISTS dpop_authserver_nonce TEXT") + pool.Exec(ctx, "ALTER TABLE oauth_sessions ADD COLUMN IF NOT EXISTS dpop_pds_nonce TEXT") + pool.Exec(ctx, "ALTER TABLE oauth_sessions ADD COLUMN IF NOT EXISTS pds_url TEXT") + pool.Exec(ctx, "ALTER TABLE oauth_sessions ADD COLUMN IF NOT EXISTS authserver_iss TEXT") + // Drop old dpop_nonce column if it exists + pool.Exec(ctx, "ALTER TABLE oauth_sessions DROP COLUMN IF EXISTS dpop_nonce") // Migration: rename feed columns for consistent terminology // last_crawled_at -> last_checked_at (feed_check = checking feeds for new items) diff --git a/oauth_handlers.go b/oauth_handlers.go index d621811..c351f14 100644 --- a/oauth_handlers.go +++ b/oauth_handlers.go @@ -194,6 +194,8 @@ func (m *OAuthManager) startOAuthFlow(w http.ResponseWriter, r *http.Request, ha // HandleCallback handles the OAuth callback func (m *OAuthManager) HandleCallback(w http.ResponseWriter, r *http.Request) { + fmt.Printf("OAuth callback: received request from %s\n", r.URL.String()) + ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() @@ -204,6 +206,17 @@ func (m *OAuthManager) HandleCallback(w http.ResponseWriter, r *http.Request) { errorParam := r.URL.Query().Get("error") errorDesc := r.URL.Query().Get("error_description") + codePreview := code + if len(codePreview) > 10 { + codePreview = codePreview[:10] + } + statePreview := state + if len(statePreview) > 10 { + statePreview = statePreview[:10] + } + fmt.Printf("OAuth callback: code=%s..., state=%s..., iss=%s, error=%s\n", + codePreview, statePreview, iss, errorParam) + // Check for errors from auth server if errorParam != "" { http.Error(w, fmt.Sprintf("Authorization error: %s - %s", errorParam, errorDesc), http.StatusBadRequest) @@ -218,9 +231,11 @@ func (m *OAuthManager) HandleCallback(w http.ResponseWriter, r *http.Request) { // Retrieve pending auth state pending := m.sessions.GetPending(state) if pending == nil { + fmt.Printf("OAuth callback: no pending state found for %s\n", state) http.Error(w, "Invalid or expired state", http.StatusBadRequest) return } + fmt.Printf("OAuth callback: found pending state for DID %s\n", pending.DID) // Verify issuer matches if iss != "" && iss != pending.AuthserverIss { @@ -236,6 +251,7 @@ func (m *OAuthManager) HandleCallback(w http.ResponseWriter, r *http.Request) { } // Exchange code for tokens + fmt.Printf("OAuth callback: exchanging code for tokens at %s\n", pending.AuthserverIss) tokenResp, err := m.client.InitialTokenRequest( ctx, code, @@ -245,36 +261,46 @@ func (m *OAuthManager) HandleCallback(w http.ResponseWriter, r *http.Request) { dpopKey, ) if err != nil { + fmt.Printf("OAuth callback: token exchange failed: %v\n", err) http.Error(w, fmt.Sprintf("Token exchange failed: %v", err), http.StatusBadRequest) return } + fmt.Printf("OAuth callback: token exchange success, sub=%s, scope=%s\n", tokenResp.Sub, tokenResp.Scope) // Verify scope if tokenResp.Scope != m.allowedScope { + fmt.Printf("OAuth callback: scope mismatch: expected %s, got %s\n", m.allowedScope, tokenResp.Scope) http.Error(w, fmt.Sprintf("Invalid scope: expected %s, got %s", m.allowedScope, tokenResp.Scope), http.StatusForbidden) return } // Resolve DID to handle + fmt.Printf("OAuth callback: resolving DID %s to handle\n", tokenResp.Sub) handle, err := resolveDIDToHandle(ctx, tokenResp.Sub) if err != nil { + fmt.Printf("OAuth callback: failed to resolve handle: %v\n", err) http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusInternalServerError) return } + fmt.Printf("OAuth callback: resolved handle: %s\n", handle) // CRITICAL: Verify user is allowed if !allowedHandles[handle] { - fmt.Printf("OAuth: access denied for handle: %s\n", handle) + fmt.Printf("OAuth callback: access denied for handle: %s (allowed: %v)\n", handle, allowedHandles) http.Error(w, "Access denied.", http.StatusForbidden) return } + fmt.Printf("OAuth callback: handle %s is allowed\n", handle) // Create session + fmt.Printf("OAuth callback: creating session for %s\n", handle) session, err := m.sessions.CreateSession(tokenResp.Sub, handle) if err != nil { + fmt.Printf("OAuth callback: failed to create session: %v\n", err) http.Error(w, fmt.Sprintf("Failed to create session: %v", err), http.StatusInternalServerError) return } + fmt.Printf("OAuth callback: session created with ID %s\n", session.ID) // Store token info in session session.AccessToken = tokenResp.AccessToken @@ -287,12 +313,15 @@ func (m *OAuthManager) HandleCallback(w http.ResponseWriter, r *http.Request) { m.sessions.UpdateSession(session) // Set session cookie + fmt.Printf("OAuth callback: setting session cookie\n") if err := m.SetSessionCookie(w, r, session.ID); err != nil { + fmt.Printf("OAuth callback: failed to set cookie: %v\n", err) http.Error(w, fmt.Sprintf("Failed to set cookie: %v", err), http.StatusInternalServerError) return } // Redirect to dashboard + fmt.Printf("OAuth callback: success! redirecting to /dashboard\n") http.Redirect(w, r, "/dashboard", http.StatusFound) } diff --git a/oauth_middleware.go b/oauth_middleware.go index a905260..55f7679 100644 --- a/oauth_middleware.go +++ b/oauth_middleware.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/json" + "fmt" "net/http" "strings" "time" @@ -15,6 +16,7 @@ func (m *OAuthManager) RequireAuth(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { session := m.GetSessionFromCookie(r) if session == nil { + fmt.Printf("RequireAuth: no session found for %s\n", r.URL.Path) // Check if this is an API call (wants JSON response) if isAPIRequest(r) { w.Header().Set("Content-Type", "application/json") diff --git a/oauth_session.go b/oauth_session.go index 9181d9f..5fbdc8d 100644 --- a/oauth_session.go +++ b/oauth_session.go @@ -304,15 +304,25 @@ func (m *OAuthManager) SetSessionCookie(w http.ResponseWriter, r *http.Request, func (m *OAuthManager) GetSessionFromCookie(r *http.Request) *OAuthSession { cookie, err := r.Cookie(sessionCookieName) if err != nil { + fmt.Printf("GetSessionFromCookie: no cookie found: %v\n", err) return nil } + fmt.Printf("GetSessionFromCookie: found cookie, length=%d\n", len(cookie.Value)) sessionID, err := decryptSessionID(cookie.Value, m.cookieSecret) if err != nil { + fmt.Printf("GetSessionFromCookie: decrypt failed: %v\n", err) return nil } + fmt.Printf("GetSessionFromCookie: decrypted session ID: %s\n", sessionID) - return m.sessions.GetSession(sessionID) + session := m.sessions.GetSession(sessionID) + if session == nil { + fmt.Printf("GetSessionFromCookie: session not found in store\n") + } else { + fmt.Printf("GetSessionFromCookie: found session for %s\n", session.Handle) + } + return session } // ClearSessionCookie removes the session cookie diff --git a/templates.go b/templates.go index 55ed57f..f46a092 100644 --- a/templates.go +++ b/templates.go @@ -445,8 +445,8 @@ const dashboardHTML = `