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',
|
token_type TEXT NOT NULL DEFAULT 'DPoP',
|
||||||
expires_at TIMESTAMP NOT NULL,
|
expires_at TIMESTAMP NOT NULL,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
dpop_nonce TEXT,
|
|
||||||
dpop_private_jwk TEXT,
|
dpop_private_jwk TEXT,
|
||||||
|
dpop_authserver_nonce TEXT,
|
||||||
|
dpop_pds_nonce TEXT,
|
||||||
|
pds_url TEXT,
|
||||||
|
authserver_iss TEXT,
|
||||||
token_expiry TIMESTAMP
|
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")
|
pool.Exec(ctx, "ALTER TABLE oauth_sessions ADD COLUMN IF NOT EXISTS token_expiry TIMESTAMP")
|
||||||
// Make access_token nullable (session created before tokens obtained)
|
// Make access_token nullable (session created before tokens obtained)
|
||||||
pool.Exec(ctx, "ALTER TABLE oauth_sessions ALTER COLUMN access_token DROP NOT NULL")
|
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
|
// Migration: rename feed columns for consistent terminology
|
||||||
// last_crawled_at -> last_checked_at (feed_check = checking feeds for new items)
|
// 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
|
// HandleCallback handles the OAuth callback
|
||||||
func (m *OAuthManager) HandleCallback(w http.ResponseWriter, r *http.Request) {
|
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)
|
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -204,6 +206,17 @@ func (m *OAuthManager) HandleCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
errorParam := r.URL.Query().Get("error")
|
errorParam := r.URL.Query().Get("error")
|
||||||
errorDesc := r.URL.Query().Get("error_description")
|
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
|
// Check for errors from auth server
|
||||||
if errorParam != "" {
|
if errorParam != "" {
|
||||||
http.Error(w, fmt.Sprintf("Authorization error: %s - %s", errorParam, errorDesc), http.StatusBadRequest)
|
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
|
// Retrieve pending auth state
|
||||||
pending := m.sessions.GetPending(state)
|
pending := m.sessions.GetPending(state)
|
||||||
if pending == nil {
|
if pending == nil {
|
||||||
|
fmt.Printf("OAuth callback: no pending state found for %s\n", state)
|
||||||
http.Error(w, "Invalid or expired state", http.StatusBadRequest)
|
http.Error(w, "Invalid or expired state", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fmt.Printf("OAuth callback: found pending state for DID %s\n", pending.DID)
|
||||||
|
|
||||||
// Verify issuer matches
|
// Verify issuer matches
|
||||||
if iss != "" && iss != pending.AuthserverIss {
|
if iss != "" && iss != pending.AuthserverIss {
|
||||||
@@ -236,6 +251,7 @@ func (m *OAuthManager) HandleCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exchange code for tokens
|
// Exchange code for tokens
|
||||||
|
fmt.Printf("OAuth callback: exchanging code for tokens at %s\n", pending.AuthserverIss)
|
||||||
tokenResp, err := m.client.InitialTokenRequest(
|
tokenResp, err := m.client.InitialTokenRequest(
|
||||||
ctx,
|
ctx,
|
||||||
code,
|
code,
|
||||||
@@ -245,36 +261,46 @@ func (m *OAuthManager) HandleCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
dpopKey,
|
dpopKey,
|
||||||
)
|
)
|
||||||
if err != nil {
|
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)
|
http.Error(w, fmt.Sprintf("Token exchange failed: %v", err), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fmt.Printf("OAuth callback: token exchange success, sub=%s, scope=%s\n", tokenResp.Sub, tokenResp.Scope)
|
||||||
|
|
||||||
// Verify scope
|
// Verify scope
|
||||||
if tokenResp.Scope != m.allowedScope {
|
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)
|
http.Error(w, fmt.Sprintf("Invalid scope: expected %s, got %s", m.allowedScope, tokenResp.Scope), http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve DID to handle
|
// Resolve DID to handle
|
||||||
|
fmt.Printf("OAuth callback: resolving DID %s to handle\n", tokenResp.Sub)
|
||||||
handle, err := resolveDIDToHandle(ctx, tokenResp.Sub)
|
handle, err := resolveDIDToHandle(ctx, tokenResp.Sub)
|
||||||
if err != nil {
|
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)
|
http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fmt.Printf("OAuth callback: resolved handle: %s\n", handle)
|
||||||
|
|
||||||
// CRITICAL: Verify user is allowed
|
// CRITICAL: Verify user is allowed
|
||||||
if !allowedHandles[handle] {
|
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)
|
http.Error(w, "Access denied.", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fmt.Printf("OAuth callback: handle %s is allowed\n", handle)
|
||||||
|
|
||||||
// Create session
|
// Create session
|
||||||
|
fmt.Printf("OAuth callback: creating session for %s\n", handle)
|
||||||
session, err := m.sessions.CreateSession(tokenResp.Sub, handle)
|
session, err := m.sessions.CreateSession(tokenResp.Sub, handle)
|
||||||
if err != nil {
|
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)
|
http.Error(w, fmt.Sprintf("Failed to create session: %v", err), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fmt.Printf("OAuth callback: session created with ID %s\n", session.ID)
|
||||||
|
|
||||||
// Store token info in session
|
// Store token info in session
|
||||||
session.AccessToken = tokenResp.AccessToken
|
session.AccessToken = tokenResp.AccessToken
|
||||||
@@ -287,12 +313,15 @@ func (m *OAuthManager) HandleCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
m.sessions.UpdateSession(session)
|
m.sessions.UpdateSession(session)
|
||||||
|
|
||||||
// Set session cookie
|
// Set session cookie
|
||||||
|
fmt.Printf("OAuth callback: setting session cookie\n")
|
||||||
if err := m.SetSessionCookie(w, r, session.ID); err != nil {
|
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)
|
http.Error(w, fmt.Sprintf("Failed to set cookie: %v", err), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to dashboard
|
// Redirect to dashboard
|
||||||
|
fmt.Printf("OAuth callback: success! redirecting to /dashboard\n")
|
||||||
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -15,6 +16,7 @@ func (m *OAuthManager) RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := m.GetSessionFromCookie(r)
|
session := m.GetSessionFromCookie(r)
|
||||||
if session == nil {
|
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)
|
// Check if this is an API call (wants JSON response)
|
||||||
if isAPIRequest(r) {
|
if isAPIRequest(r) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
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 {
|
func (m *OAuthManager) GetSessionFromCookie(r *http.Request) *OAuthSession {
|
||||||
cookie, err := r.Cookie(sessionCookieName)
|
cookie, err := r.Cookie(sessionCookieName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Printf("GetSessionFromCookie: no cookie found: %v\n", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
fmt.Printf("GetSessionFromCookie: found cookie, length=%d\n", len(cookie.Value))
|
||||||
|
|
||||||
sessionID, err := decryptSessionID(cookie.Value, m.cookieSecret)
|
sessionID, err := decryptSessionID(cookie.Value, m.cookieSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Printf("GetSessionFromCookie: decrypt failed: %v\n", err)
|
||||||
return nil
|
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
|
// ClearSessionCookie removes the session cookie
|
||||||
|
|||||||
+2
-2
@@ -445,8 +445,8 @@ const dashboardHTML = `<!DOCTYPE html>
|
|||||||
<title>1440.news Feed Crawler</title>
|
<title>1440.news Feed Crawler</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="/static/dashboard.css?v=1770006945">
|
<link rel="stylesheet" href="/static/dashboard.css?v=1770011013">
|
||||||
<script src="/static/dashboard.js?v=1770006945"></script>
|
<script src="/static/dashboard.js?v=1770011013"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="topSection">
|
<div id="topSection">
|
||||||
|
|||||||
Reference in New Issue
Block a user