This commit is contained in:
2025-08-26 13:57:40 -04:00
parent 39e850f8ac
commit 2a9debd9b3
3 changed files with 23 additions and 87 deletions

View File

@ -124,20 +124,11 @@ type VerifyResponse struct {
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
// TokenDeliveryMode specifies how tokens should be delivered in redirect flows
type TokenDeliveryMode string
const (
TokenDeliveryCookie TokenDeliveryMode = "cookie" // Token in secure cookie (default)
TokenDeliveryQuery TokenDeliveryMode = "query" // Token in query parameter (for integrations)
)
// LoginRequest represents a user login request // LoginRequest represents a user login request
type LoginRequest struct { type LoginRequest struct {
AppID string `json:"app_id" validate:"required"` AppID string `json:"app_id" validate:"required"`
Permissions []string `json:"permissions,omitempty"` Permissions []string `json:"permissions,omitempty"`
RedirectURI string `json:"redirect_uri,omitempty"` RedirectURI string `json:"redirect_uri,omitempty"`
TokenDelivery TokenDeliveryMode `json:"token_delivery,omitempty"` // How to deliver token in redirect flows
} }
// LoginResponse represents a user login response // LoginResponse represents a user login response

View File

@ -35,13 +35,12 @@ type AuthHandler struct {
// LoginPageData represents data passed to the login HTML template // LoginPageData represents data passed to the login HTML template
type LoginPageData struct { type LoginPageData struct {
Token string Token string
TokenJSON template.JS TokenJSON template.JS
RedirectURLJSON template.JS RedirectURLJSON template.JS
TokenDeliveryJSON template.JS ExpiresAt string
ExpiresAt string AppID string
AppID string UserID string
UserID string
} }
// NewAuthHandler creates a new auth handler // NewAuthHandler creates a new auth handler
@ -92,7 +91,6 @@ func (h *AuthHandler) Login(c *gin.Context) {
// Handle HTML request (GET or POST with form data) // Handle HTML request (GET or POST with form data)
req.AppID = c.Query("app_id") req.AppID = c.Query("app_id")
req.RedirectURI = c.Query("redirect_uri") req.RedirectURI = c.Query("redirect_uri")
req.TokenDelivery = domain.TokenDeliveryMode(c.DefaultQuery("token_delivery", string(domain.TokenDeliveryQuery)))
// Parse permissions from query parameter (comma-separated) // Parse permissions from query parameter (comma-separated)
if perms := c.Query("permissions"); perms != "" { if perms := c.Query("permissions"); perms != "" {
@ -142,56 +140,12 @@ func (h *AuthHandler) Login(c *gin.Context) {
return return
} }
// Handle redirect flows // Handle redirect flows - always deliver token via query parameter
tokenDelivery := req.TokenDelivery
if tokenDelivery == "" {
// 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)))
// Generate a secure state parameter for CSRF protection
state := h.generateSecureState(userContext.UserID, req.AppID)
var redirectURL string var redirectURL string
if req.RedirectURI != "" {
switch tokenDelivery { // Generate a secure state parameter for CSRF protection
case domain.TokenDeliveryQuery: state := h.generateSecureState(userContext.UserID, req.AppID)
// 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)
c.SetSameSite(http.SameSiteStrictMode)
// In development mode, make cookie accessible to JavaScript for testing
// In production, keep HTTP-only for security
httpOnly := !h.config.IsDevelopment()
secure := !h.config.IsDevelopment() // Only require HTTPS in production
c.SetCookie(
"auth_token", // name
token, // value
604800, // maxAge (7 days)
"/", // path
"", // domain (empty for current domain)
secure, // secure (HTTPS only in production)
httpOnly, // httpOnly (no JavaScript access in production)
)
// Redirect without token in URL for security
if req.RedirectURI != "" {
redirectURL = req.RedirectURI + "?state=" + state
}
default:
// Invalid delivery mode, default to cookie
if req.RedirectURI != "" {
redirectURL = req.RedirectURI + "?state=" + state
}
} }
// Return appropriate response format // Return appropriate response format
@ -202,12 +156,12 @@ func (h *AuthHandler) Login(c *gin.Context) {
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
} else { } else {
// Render HTML page // Render HTML page
h.renderLoginPage(c, token, redirectURL, string(tokenDelivery), userContext.UserID, req.AppID) h.renderLoginPage(c, token, redirectURL, userContext.UserID, req.AppID)
} }
} }
// renderLoginPage renders the HTML login page with token information // renderLoginPage renders the HTML login page with token information
func (h *AuthHandler) renderLoginPage(c *gin.Context, token, redirectURL, tokenDelivery, userID, appID string) { func (h *AuthHandler) renderLoginPage(c *gin.Context, token, redirectURL, userID, appID string) {
if h.loginTemplate == nil { if h.loginTemplate == nil {
// Fallback to JSON if template not available // Fallback to JSON if template not available
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@ -223,16 +177,14 @@ func (h *AuthHandler) renderLoginPage(c *gin.Context, token, redirectURL, tokenD
// Prepare template data // Prepare template data
tokenJSON, _ := json.Marshal(token) tokenJSON, _ := json.Marshal(token)
redirectURLJSON, _ := json.Marshal(redirectURL) redirectURLJSON, _ := json.Marshal(redirectURL)
tokenDeliveryJSON, _ := json.Marshal(tokenDelivery)
data := LoginPageData{ data := LoginPageData{
Token: token, Token: token,
TokenJSON: template.JS(tokenJSON), TokenJSON: template.JS(tokenJSON),
RedirectURLJSON: template.JS(redirectURLJSON), 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"),
ExpiresAt: time.Now().Add(7 * 24 * time.Hour).Format("Jan 2, 2006 at 3:04 PM MST"), AppID: appID,
AppID: appID, UserID: userID,
UserID: userID,
} }
c.Header("Content-Type", "text/html; charset=utf-8") c.Header("Content-Type", "text/html; charset=utf-8")

View File

@ -157,7 +157,6 @@
// Token and redirect information from server // Token and redirect information from server
const token = {{.TokenJSON}}; const token = {{.TokenJSON}};
const redirectURL = {{.RedirectURLJSON}}; const redirectURL = {{.RedirectURLJSON}};
const tokenDelivery = {{.TokenDeliveryJSON}};
// Elements // Elements
const loadingDiv = document.getElementById('loading'); const loadingDiv = document.getElementById('loading');
@ -229,13 +228,7 @@
setTimeout(performRedirect, 1000); setTimeout(performRedirect, 1000);
}); });
// Handle cookie-based token delivery // Token is always delivered via query parameter
if (tokenDelivery === 'cookie') {
document.addEventListener('DOMContentLoaded', function() {
const message = document.querySelector('.redirect-info');
message.innerHTML += '<br><strong>Delivery method:</strong> Secure cookie (auth_token)';
});
}
</script> </script>
</body> </body>
</html> </html>