2025-04-27 16:59:35 -04:00
# auth/callback_server.py
import http.server
import logging
2025-05-10 17:57:25 -04:00
import os
2025-04-27 16:59:35 -04:00
import socketserver
import threading
import urllib.parse
import webbrowser
2025-05-10 17:57:25 -04:00
from typing import Callable , Optional , Dict , Any , Literal
from oauthlib.oauth2.rfc6749.errors import InsecureTransportError
2025-04-27 16:59:35 -04:00
# Configure logging
logging . basicConfig ( level = logging . INFO )
logger = logging . getLogger ( __name__ )
2025-05-10 17:57:25 -04:00
def get_response_html (
title : str ,
status : Literal [ "success" , "error" ],
message : str ,
show_close_button : bool = True ,
auto_close_seconds : int = 0
) -> str :
"""Generate HTML response for OAuth callback.
Args:
title: Page title
status: 'success' or 'error'
message: Message to display to the user
show_close_button: Whether to show a close button
auto_close_seconds: Auto-close after this many seconds (0 to disable)
Returns:
HTML content as a string
"""
color = "#4CAF50" if status == "success" else "#d32f2f"
close_button = """
<button class="button" onclick="window.close()">Close Window</button>
""" if show_close_button else ""
auto_close_script = f """
<script>
setTimeout(function() {{ window.close(); }} , { auto_close_seconds * 1000 } );
</script>
""" if auto_close_seconds > 0 else ""
return f """<!DOCTYPE html>
<html>
<head>
<title> { title } </title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 500px;
margin: 40px auto;
padding: 20px;
text-align: center;
color: #333;
}}
.status {{
color: { color } ;
font-size: 24px;
margin-bottom: 20px;
}}
.message {{
margin-bottom: 30px;
line-height: 1.5;
}}
.button {{
background-color: { color } ;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}}
</style>
</head>
<body>
2025-05-10 18:50:01 -04:00
<div class="status"> { title } </div>
2025-05-10 17:57:25 -04:00
<div class="message"> { message } </div>
{ close_button }
{ auto_close_script }
</body>
</html>"""
2025-04-27 16:59:35 -04:00
class OAuthCallbackHandler ( http . server . BaseHTTPRequestHandler ):
"""Handler for OAuth callback requests."""
2025-05-10 17:57:25 -04:00
# Class variables to store callback functions by state
callback_registry : Dict [ str , Callable ] = {}
@classmethod
def register_callback ( cls , state : str , callback : Callable ) -> None :
"""Register a callback function for a specific state parameter."""
logger . info ( f "Registering callback for state: { state } " )
cls . callback_registry [ state ] = callback
@classmethod
def unregister_callback ( cls , state : str ) -> None :
"""Unregister a callback function for a specific state parameter."""
if state in cls . callback_registry :
logger . info ( f "Unregistering callback for state: { state } " )
del cls . callback_registry [ state ]
2025-04-27 16:59:35 -04:00
def do_GET ( self ):
"""Handle GET requests to the callback endpoint."""
request_thread_id = threading . get_ident ()
logger . info ( f "[Handler { request_thread_id } ] GET request received for path: { self . path } " )
try :
# Parse the URL and extract query parameters
parsed_url = urllib . parse . urlparse ( self . path )
query_params = urllib . parse . parse_qs ( parsed_url . query )
# Check if we're handling the OAuth callback
if parsed_url . path == '/callback' :
# Extract authorization code and state
code = query_params . get ( 'code' , [ '' ])[ 0 ]
state = query_params . get ( 'state' , [ '' ])[ 0 ]
logger . info ( f "Received OAuth callback with code: { code [: 10 ] } ... and state: { state } " )
# Show success page to the user
self . send_response ( 200 )
self . send_header ( 'Content-Type' , 'text/html' )
self . end_headers ()
2025-05-10 17:57:25 -04:00
html_content = get_response_html (
title = "Authentication Successful" ,
status = "success" ,
message = "You have successfully authenticated with Google. You can now close this window and return to your application." ,
show_close_button = True ,
auto_close_seconds = 10
)
2025-04-27 16:59:35 -04:00
self . wfile . write ( html_content . encode ())
2025-05-10 17:57:25 -04:00
try :
# Ensure OAUTHLIB_INSECURE_TRANSPORT is set
if 'OAUTHLIB_INSECURE_TRANSPORT' not in os . environ :
logger . warning ( "OAUTHLIB_INSECURE_TRANSPORT not set. Setting it for localhost development." )
os . environ [ 'OAUTHLIB_INSECURE_TRANSPORT' ] = '1'
# Call the appropriate callback function based on state
if state in OAuthCallbackHandler . callback_registry and code :
logger . info ( f "[Handler { request_thread_id } ] Found callback for state: { state } " )
callback_function = OAuthCallbackHandler . callback_registry [ state ]
logger . info ( f "[Handler { request_thread_id } ] Preparing to call callback function in new thread." )
callback_thread = threading . Thread (
target = callback_function ,
args = ( code , state ),
daemon = True
)
callback_thread . start ()
logger . info ( f "[Handler { request_thread_id } ] Callback function thread started (ID: { callback_thread . ident } )." )
# Unregister the callback after it's been called
OAuthCallbackHandler . unregister_callback ( state )
else :
logger . warning ( f "[Handler { request_thread_id } ] No callback registered for state: { state } or no code received." )
except InsecureTransportError as e :
logger . error ( f "[Handler { request_thread_id } ] InsecureTransportError: { e } . Ensure OAUTHLIB_INSECURE_TRANSPORT is set for localhost development." )
self . send_response ( 400 )
self . send_header ( 'Content-Type' , 'text/html' )
self . end_headers ()
error_html = get_response_html (
title = "OAuth Error: Insecure Transport" ,
status = "error" ,
message = "The OAuth flow requires HTTPS or explicit allowance of HTTP for localhost development. Please ensure OAUTHLIB_INSECURE_TRANSPORT is set in your environment." ,
show_close_button = False
2025-04-27 16:59:35 -04:00
)
2025-05-10 17:57:25 -04:00
self . wfile . write ( error_html . encode ())
return
2025-04-27 16:59:35 -04:00
2025-05-10 17:57:25 -04:00
# Note: We no longer shut down the server after handling a callback
# This allows it to handle multiple auth flows over time
2025-04-27 16:59:35 -04:00
else :
# Handle other paths with a 404 response
self . send_response ( 404 )
2025-05-10 17:57:25 -04:00
self . send_header ( 'Content-Type' , 'text/html' )
2025-04-27 16:59:35 -04:00
self . end_headers ()
2025-05-10 17:57:25 -04:00
error_html = get_response_html (
title = "Not Found" ,
status = "error" ,
message = "The requested resource was not found." ,
show_close_button = False
)
self . wfile . write ( error_html . encode ())
2025-04-27 16:59:35 -04:00
except Exception as e :
logger . error ( f "[Handler { request_thread_id } ] Error handling callback request: { e } " , exc_info = True )
try :
self . send_response ( 500 )
2025-05-10 17:57:25 -04:00
self . send_header ( 'Content-Type' , 'text/html' )
2025-04-27 16:59:35 -04:00
self . end_headers ()
2025-05-10 17:57:25 -04:00
error_html = get_response_html (
title = "Internal Server Error" ,
status = "error" ,
message = f "An error occurred while processing your request: { str ( e ) } " ,
show_close_button = False
)
self . wfile . write ( error_html . encode ())
2025-04-27 16:59:35 -04:00
except Exception as send_error :
2025-05-10 17:57:25 -04:00
logger . error ( f "[Handler { request_thread_id } ] Error sending 500 response: { send_error } " )
2025-04-27 16:59:35 -04:00
def log_message ( self , format , * args ):
"""Override to use our logger instead of printing to stderr."""
logger . info ( f " { self . address_string () } - { format % args } " )
class OAuthCallbackServer :
"""Server to handle OAuth callbacks."""
2025-05-10 17:57:25 -04:00
def __init__ ( self ,
port : int = 8080 ,
2025-04-27 16:59:35 -04:00
auto_open_browser : bool = True ):
"""
Initialize the callback server.
Args:
port: Port to listen on (default: 8080)
auto_open_browser: Whether to automatically open the browser
"""
self . port = port
self . server = None
self . server_thread = None
self . auto_open_browser = auto_open_browser
2025-05-10 17:57:25 -04:00
def start ( self ) -> Dict [ str , Any ]:
"""
Start the callback server in a separate thread.
Returns:
Dict containing server status and port information:
{
'success': bool,
'port': int,
'message': str
}
"""
2025-04-27 16:59:35 -04:00
if self . server :
logger . warning ( "Server is already running" )
2025-05-10 17:57:25 -04:00
return { 'success' : False , 'port' : self . port , 'message' : 'Server is already running' }
2025-04-27 16:59:35 -04:00
2025-05-10 17:57:25 -04:00
original_port = self . port
max_port = 8090 # Try ports 8080-8090
def serve ():
thread_id = threading . get_ident ()
logger . info ( f "[Server Thread { thread_id } ] Starting serve_forever loop." )
try :
self . server . serve_forever ()
except Exception as serve_e :
logger . error ( f "[Server Thread { thread_id } ] Exception in serve_forever: { serve_e } " , exc_info = True )
finally :
logger . info ( f "[Server Thread { thread_id } ] serve_forever loop finished." )
# Ensure server_close is called even if shutdown wasn't clean
2025-04-27 16:59:35 -04:00
try :
2025-05-10 17:57:25 -04:00
if self . server :
self . server . server_close ()
except Exception as close_e :
logger . error ( f "[Server Thread { thread_id } ] Error during server_close: { close_e } " )
2025-04-27 16:59:35 -04:00
2025-05-10 17:57:25 -04:00
try :
while self . port <= max_port :
try :
# Create and start the server
self . server = socketserver . TCPServer (( 'localhost' , self . port ), OAuthCallbackHandler )
logger . info ( f "Starting OAuth callback server on port { self . port } " )
if self . port != original_port :
logger . info ( f "Successfully reassigned from port { original_port } to { self . port } " )
# Start the server thread
self . server_thread = threading . Thread ( target = serve , daemon = True )
self . server_thread . start ()
logger . info ( f "OAuth callback server thread started (ID: { self . server_thread . ident } ) on http://localhost: { self . port } " )
return {
'success' : True ,
'port' : self . port ,
'message' : f "Server started successfully on port { self . port } "
}
except OSError as e :
if e . errno == 48 : # Address already in use
logger . warning ( f "Port { self . port } is already in use, trying next port" )
self . port += 1
if self . port > max_port :
error_msg = f "Failed to find available port in range { original_port } - { max_port } "
logger . error ( error_msg )
return { 'success' : False , 'port' : None , 'message' : error_msg }
continue
else :
logger . error ( f "Failed to start server: { e } " )
return { 'success' : False , 'port' : None , 'message' : str ( e )}
2025-04-27 16:59:35 -04:00
except Exception as e :
2025-05-10 17:57:25 -04:00
error_msg = f "Failed to start callback server: { e } "
logger . error ( error_msg )
return { 'success' : False , 'port' : None , 'message' : error_msg }
2025-04-27 16:59:35 -04:00
def stop ( self ) -> None :
"""Stop the callback server."""
if self . server :
logger . info ( "Stopping OAuth callback server" )
# shutdown() signals serve_forever to stop
self . server . shutdown ()
logger . info ( "Server shutdown() called." )
# Wait briefly for the server thread to finish
if self . server_thread :
2025-05-10 17:57:25 -04:00
self . server_thread . join ( timeout = 2.0 ) # Wait up to 2 seconds
if self . server_thread . is_alive ():
logger . warning ( "Server thread did not exit cleanly after shutdown." )
2025-04-27 16:59:35 -04:00
# server_close() is now called in the 'finally' block of the serve() function
self . server = None
self . server_thread = None
logger . info ( "Server resources released." )
else :
logger . warning ( "Server is not running" )
def open_browser ( self , url : str ) -> bool :
"""Open the default web browser to the given URL."""
if not self . auto_open_browser :
return False
try :
logger . info ( f "Opening browser to: { url } " )
webbrowser . open ( url )
return True
except Exception as e :
logger . error ( f "Failed to open browser: { e } " )
return False