This commit is contained in:
2025-08-26 13:51:15 -04:00
parent e1c7e825af
commit 39e850f8ac
6 changed files with 705 additions and 19 deletions

View File

@ -5,8 +5,11 @@ import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"html/template"
"net/http"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
@ -27,6 +30,18 @@ type AuthHandler struct {
config config.ConfigProvider
errorHandler *errors.ErrorHandler
logger *zap.Logger
loginTemplate *template.Template
}
// LoginPageData represents data passed to the login HTML template
type LoginPageData struct {
Token string
TokenJSON template.JS
RedirectURLJSON template.JS
TokenDeliveryJSON template.JS
ExpiresAt string
AppID string
UserID string
}
// NewAuthHandler creates a new auth handler
@ -36,6 +51,14 @@ func NewAuthHandler(
config config.ConfigProvider,
logger *zap.Logger,
) *AuthHandler {
// Load login template
templatePath := filepath.Join("templates", "login.html")
loginTemplate, err := template.ParseFiles(templatePath)
if err != nil {
logger.Error("Failed to load login template", zap.Error(err), zap.String("path", templatePath))
// Template loading failure is not fatal, we'll fall back to JSON
}
return &AuthHandler{
authService: authService,
tokenService: tokenService,
@ -43,21 +66,55 @@ func NewAuthHandler(
config: config,
errorHandler: errors.NewErrorHandler(logger),
logger: logger,
loginTemplate: loginTemplate,
}
}
// Login handles POST /login
// Login handles login requests (both GET for HTML and POST for JSON)
func (h *AuthHandler) Login(c *gin.Context) {
// Handle GET requests or requests that prefer HTML
acceptHeader := c.GetHeader("Accept")
contentType := c.GetHeader("Content-Type")
isJSONRequest := (c.Request.Method == "POST" && (contentType == "application/json" ||
(acceptHeader != "" && (acceptHeader == "application/json" ||
(acceptHeader != "text/html" && acceptHeader != "*/*")))))
var req domain.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.errorHandler.HandleValidationError(c, "request_body", "Invalid login request format")
return
if isJSONRequest {
// Handle JSON POST request (existing API behavior)
if err := c.ShouldBindJSON(&req); err != nil {
h.errorHandler.HandleValidationError(c, "request_body", "Invalid login request format")
return
}
} else {
// Handle HTML request (GET or POST with form data)
req.AppID = c.Query("app_id")
req.RedirectURI = c.Query("redirect_uri")
req.TokenDelivery = domain.TokenDeliveryMode(c.DefaultQuery("token_delivery", string(domain.TokenDeliveryQuery)))
// Parse permissions from query parameter (comma-separated)
if perms := c.Query("permissions"); perms != "" {
// Simple parsing for comma-separated permissions
req.Permissions = []string{perms} // Simplified for this example
}
// If no app_id provided, show error
if req.AppID == "" {
h.renderLoginError(c, "Missing required parameter: app_id", isJSONRequest)
return
}
}
// Validate authentication headers with HMAC signature
userContext, err := h.headerValidator.ValidateAuthenticationHeaders(c.Request)
if err != nil {
h.errorHandler.HandleAuthenticationError(c, err)
if isJSONRequest {
h.errorHandler.HandleAuthenticationError(c, err)
} else {
h.renderLoginError(c, "Authentication failed: "+err.Error(), isJSONRequest)
}
return
}
@ -66,12 +123,16 @@ func (h *AuthHandler) Login(c *gin.Context) {
// Generate user token
token, err := h.tokenService.GenerateUserToken(c.Request.Context(), req.AppID, userContext.UserID, req.Permissions)
if err != nil {
h.errorHandler.HandleInternalError(c, err)
if isJSONRequest {
h.errorHandler.HandleInternalError(c, err)
} else {
h.renderLoginError(c, "Failed to generate token: "+err.Error(), isJSONRequest)
}
return
}
if req.RedirectURI == "" {
// If no redirect URI, return token directly via secure response body
// For JSON requests without redirect URI, return token directly
if isJSONRequest && req.RedirectURI == "" {
c.JSON(http.StatusOK, gin.H{
"token": token,
"user_id": userContext.UserID,
@ -81,11 +142,12 @@ func (h *AuthHandler) Login(c *gin.Context) {
return
}
// For redirect flows, choose token delivery method
// Default to cookie delivery for security
// Handle redirect flows
tokenDelivery := req.TokenDelivery
if tokenDelivery == "" {
tokenDelivery = domain.TokenDeliveryCookie
// Default to query delivery for redirects (external apps need token in URL)
// Only use cookie delivery if explicitly specified
tokenDelivery = domain.TokenDeliveryQuery
}
h.logger.Debug("Token delivery mode", zap.String("mode", string(tokenDelivery)))
@ -97,7 +159,9 @@ func (h *AuthHandler) Login(c *gin.Context) {
switch tokenDelivery {
case domain.TokenDeliveryQuery:
// Deliver token via query parameter (for integrations like VS Code)
redirectURL = req.RedirectURI + "?token=" + token + "&state=" + state
if req.RedirectURI != "" {
redirectURL = req.RedirectURI + "?token=" + token + "&state=" + state
}
case domain.TokenDeliveryCookie:
// Deliver token via secure cookie (default, more secure)
@ -119,18 +183,105 @@ func (h *AuthHandler) Login(c *gin.Context) {
)
// Redirect without token in URL for security
redirectURL = req.RedirectURI + "?state=" + state
if req.RedirectURI != "" {
redirectURL = req.RedirectURI + "?state=" + state
}
default:
// Invalid delivery mode, default to cookie
redirectURL = req.RedirectURI + "?state=" + state
if req.RedirectURI != "" {
redirectURL = req.RedirectURI + "?state=" + state
}
}
response := domain.LoginResponse{
RedirectURL: redirectURL,
// Return appropriate response format
if isJSONRequest {
response := domain.LoginResponse{
RedirectURL: redirectURL,
}
c.JSON(http.StatusOK, response)
} else {
// Render HTML page
h.renderLoginPage(c, token, redirectURL, string(tokenDelivery), userContext.UserID, req.AppID)
}
}
// renderLoginPage renders the HTML login page with token information
func (h *AuthHandler) renderLoginPage(c *gin.Context, token, redirectURL, tokenDelivery, userID, appID string) {
if h.loginTemplate == nil {
// Fallback to JSON if template not available
c.JSON(http.StatusOK, gin.H{
"token": token,
"redirect_url": redirectURL,
"user_id": userID,
"app_id": appID,
"message": "Login successful - HTML template not available",
})
return
}
c.JSON(http.StatusOK, response)
// Prepare template data
tokenJSON, _ := json.Marshal(token)
redirectURLJSON, _ := json.Marshal(redirectURL)
tokenDeliveryJSON, _ := json.Marshal(tokenDelivery)
data := LoginPageData{
Token: token,
TokenJSON: template.JS(tokenJSON),
RedirectURLJSON: template.JS(redirectURLJSON),
TokenDeliveryJSON: template.JS(tokenDeliveryJSON),
ExpiresAt: time.Now().Add(7 * 24 * time.Hour).Format("Jan 2, 2006 at 3:04 PM MST"),
AppID: appID,
UserID: userID,
}
c.Header("Content-Type", "text/html; charset=utf-8")
// Override CSP for login page to allow inline styles and scripts
c.Header("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'")
if err := h.loginTemplate.Execute(c.Writer, data); err != nil {
h.logger.Error("Failed to render login template", zap.Error(err))
// Fallback to JSON response
c.JSON(http.StatusOK, gin.H{
"token": token,
"redirect_url": redirectURL,
"user_id": userID,
"app_id": appID,
"message": "Login successful - template render failed",
})
}
}
// renderLoginError renders an error page or JSON error response
func (h *AuthHandler) renderLoginError(c *gin.Context, message string, isJSON bool) {
if isJSON {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": message,
})
return
}
// Simple HTML error page
c.Header("Content-Type", "text/html; charset=utf-8")
// Override CSP for error page to allow inline styles
c.Header("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'")
c.String(http.StatusBadRequest, `
<!DOCTYPE html>
<html>
<head>
<title>Login Error</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
.error { background: #f8d7da; color: #721c24; padding: 15px; border-radius: 5px; }
</style>
</head>
<body>
<h1>Login Error</h1>
<div class="error">%s</div>
<p><a href="javascript:history.back()">Go back</a></p>
</body>
</html>`, message)
}
// generateSecureState generates a secure state parameter for OAuth flows