Files
atlantis/server/controllers/auth_controller.go
PePe Amengual fc45de0296 Fix all linter errors and warnings in auth package
- Add missing comments for exported types and constants
- Remove unused Middleware struct and ManagerInterface references
- Fix all unused parameter warnings in test mocks and handlers
- Ensure all typecheck errors are resolved
- Update tests to use correct field names

All SSO auth code and tests are now lint-clean and typecheck clean.
2025-06-27 10:46:59 -07:00

262 lines
8.2 KiB
Go

// Package controllers provides HTTP controllers for handling web requests in Atlantis.
// This includes authentication controllers for managing user login, logout, and session management.
package controllers
import (
"crypto/rand"
"encoding/base64"
"fmt"
"html/template"
"net/http"
"net/url"
"github.com/runatlantis/atlantis/server/auth"
"github.com/runatlantis/atlantis/server/logging"
)
var loginTemplate = template.Must(template.New("login").Parse(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Atlantis - Login</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{.BasePath}}/static/css/normalize.css">
<link rel="stylesheet" href="{{.BasePath}}/static/css/skeleton.css">
<link rel="stylesheet" href="{{.BasePath}}/static/css/custom.css">
<link rel="icon" type="image/png" href="{{.BasePath}}/static/images/atlantis-icon.png">
</head>
<body>
<div class="container">
<section class="header">
<a title="atlantis" href="{{.BasePath}}/"><img class="hero" src="{{.BasePath}}/static/images/atlantis-icon_512.png"/></a>
<p class="title-heading">Atlantis</p>
<p class="title-heading"><strong>Login</strong></p>
</section>
<section>
<div class="row">
<div class="six columns offset-by-three">
<h5>Choose your login method:</h5>
{{range .Providers}}
<a class="button button-primary u-full-width" href="{{.AuthURL}}">
Login with {{.Name}}
</a>
{{end}}
</div>
</div>
</section>
</div>
<footer>
<p>Atlantis SSO Authentication</p>
</footer>
</body>
</html>
`))
type loginProviderData struct {
Name string
AuthURL string
}
type loginPageData struct {
BasePath string
Providers []loginProviderData
}
// AuthController handles authentication-related HTTP requests
type AuthController struct {
AuthManager auth.Manager
Logger logging.SimpleLogging
BaseURL *url.URL
}
// NewAuthController creates a new authentication controller
func NewAuthController(authManager auth.Manager, logger logging.SimpleLogging, baseURL *url.URL) *AuthController {
return &AuthController{
AuthManager: authManager,
Logger: logger,
BaseURL: baseURL,
}
}
// Login handles the login page
func (c *AuthController) Login(w http.ResponseWriter, r *http.Request) {
providers := c.AuthManager.GetEnabledProviders()
// If only one provider and it's OAuth2/OIDC, redirect directly
if len(providers) == 1 {
provider := providers[0]
if provider.GetType() == auth.ProviderTypeOAuth2 || provider.GetType() == auth.ProviderTypeOIDC {
c.redirectToProvider(w, r, provider)
return
}
}
// Show simple login page
c.showLoginPage(w, r, providers)
}
// Callback handles OAuth2/OIDC callback
func (c *AuthController) Callback(w http.ResponseWriter, r *http.Request) {
// Get provider from query parameter
providerID := r.URL.Query().Get("provider")
if providerID == "" {
c.respond(w, logging.Error, http.StatusBadRequest, "Missing provider parameter")
return
}
provider, err := c.AuthManager.GetProvider(providerID)
if err != nil {
c.respond(w, logging.Error, http.StatusBadRequest, "Invalid provider: %s", err)
return
}
// Handle OAuth2/OIDC callback
if provider.GetType() == auth.ProviderTypeOAuth2 || provider.GetType() == auth.ProviderTypeOIDC {
c.handleOAuthCallback(w, r, provider)
return
}
c.respond(w, logging.Error, http.StatusBadRequest, "Unsupported provider type for callback")
}
// Logout handles user logout
func (c *AuthController) Logout(w http.ResponseWriter, r *http.Request) {
// Get user from request to invalidate their session
if user, err := c.AuthManager.GetUserFromRequest(r); err == nil {
if user.SessionID != "" {
if err := c.AuthManager.InvalidateSession(r.Context(), user.SessionID); err != nil {
c.Logger.Err("Failed to invalidate session: %v", err)
}
}
}
// Clear session cookie
if authManager, ok := c.AuthManager.(*auth.AuthManager); ok {
authManager.ClearSessionCookie(w)
}
// Redirect to login page
http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect)
}
// redirectToProvider redirects to a specific OAuth2/OIDC provider
func (c *AuthController) redirectToProvider(w http.ResponseWriter, r *http.Request, provider auth.Provider) {
state, err := c.generateState()
if err != nil {
c.respond(w, logging.Error, http.StatusInternalServerError, "Failed to generate state: %s", err)
return
}
authURL, err := provider.InitAuthURL(state)
if err != nil {
c.respond(w, logging.Error, http.StatusInternalServerError, "Failed to generate auth URL: %s", err)
return
}
http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
}
// showLoginPage displays a simple login page with available providers
func (c *AuthController) showLoginPage(w http.ResponseWriter, _ *http.Request, providers []auth.Provider) {
var providerData []loginProviderData
for _, provider := range providers {
if provider.GetType() == auth.ProviderTypeOAuth2 || provider.GetType() == auth.ProviderTypeOIDC {
state, err := c.generateState()
if err != nil {
c.Logger.Err("Failed to generate state for provider %s: %s", provider.GetID(), err)
continue
}
authURL, err := provider.InitAuthURL(state)
if err != nil {
c.Logger.Err("Failed to generate auth URL for provider %s: %s", provider.GetID(), err)
continue
}
providerData = append(providerData, loginProviderData{
Name: provider.GetName(),
AuthURL: authURL,
})
}
}
data := loginPageData{
BasePath: c.BaseURL.Path,
Providers: providerData,
}
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
if err := loginTemplate.Execute(w, data); err != nil {
c.Logger.Err("Failed to render login template: %s", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// handleOAuthCallback processes OAuth2/OIDC callback
func (c *AuthController) handleOAuthCallback(w http.ResponseWriter, r *http.Request, provider auth.Provider) {
// Get authorization code
code := r.URL.Query().Get("code")
if code == "" {
c.respond(w, logging.Error, http.StatusBadRequest, "Missing authorization code")
return
}
// Get state parameter for CSRF protection
state := r.URL.Query().Get("state")
if state == "" {
c.respond(w, logging.Error, http.StatusBadRequest, "Missing state parameter")
return
}
// Exchange code for tokens
token, err := provider.ExchangeCode(r.Context(), code)
if err != nil {
c.Logger.Err("Failed to exchange code for token: %s", err)
c.respond(w, logging.Error, http.StatusBadRequest, "Authentication failed")
return
}
// Get user information
user, err := provider.GetUserInfo(r.Context(), token)
if err != nil {
c.Logger.Err("Failed to get user info: %s", err)
c.respond(w, logging.Error, http.StatusBadRequest, "Failed to get user information")
return
}
// Authenticate user and create session
session, err := c.AuthManager.AuthenticateUser(r.Context(), user)
if err != nil {
c.Logger.Err("Failed to authenticate user: %s", err)
c.respond(w, logging.Error, http.StatusInternalServerError, "Failed to create session")
return
}
// Set session cookie
if authManager, ok := c.AuthManager.(*auth.AuthManager); ok {
authManager.SetSessionCookie(w, session.ID)
}
c.Logger.Info("User %s authenticated successfully via %s", user.Email, provider.GetName())
// Redirect to home page
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
// generateState generates a random state parameter for CSRF protection
func (c *AuthController) generateState() (string, error) {
// Generate a cryptographically secure random state
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate random state: %w", err)
}
return base64.URLEncoding.EncodeToString(bytes), nil
}
// respond sends an HTTP response with the given status code and message
func (c *AuthController) respond(w http.ResponseWriter, lvl logging.LogLevel, responseCode int, format string, args ...interface{}) {
response := fmt.Sprintf(format, args...)
c.Logger.Log(lvl, response)
w.WriteHeader(responseCode)
if _, err := fmt.Fprint(w, response); err != nil {
c.Logger.Err("Failed to write response: %v", err)
}
}