Fix OAuth session storage - add missing database columns
- Add dpop_authserver_nonce, dpop_pds_nonce, pds_url, authserver_iss columns - These columns are required by GetSession query but were missing from schema - Add migrations to create columns on existing tables - Add debug logging for OAuth flow troubleshooting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
+30
-1
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
+11
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -445,8 +445,8 @@ const dashboardHTML = `<!DOCTYPE html>
|
||||
<title>1440.news Feed Crawler</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/static/dashboard.css?v=1770006945">
|
||||
<script src="/static/dashboard.js?v=1770006945"></script>
|
||||
<link rel="stylesheet" href="/static/dashboard.css?v=1770011013">
|
||||
<script src="/static/dashboard.js?v=1770011013"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="topSection">
|
||||
|
||||
Reference in New Issue
Block a user