mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-19 13:00:50 +02:00
feat: Implement authentication provider management components
- Added new templates for managing authentication providers, including forms for creating and editing providers. - Implemented backend logic to handle retrieval, creation, and deletion of authentication providers. - Introduced new database migrations to support the storage of authentication provider data. - Enhanced the user interface to display available authentication providers and their statuses. - Added routes and handlers for managing authentication provider actions in the web application.
This commit is contained in:
@@ -3,10 +3,16 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -668,3 +674,526 @@ func generateResetToken(length int) (string, error) {
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// GetAuthProviders returns the list of enabled authentication providers for login
|
||||
func (h *Handlers) GetAuthProviders(c *gin.Context) {
|
||||
providers, err := h.DB.GetEnabledAuthProviders(c.Request.Context())
|
||||
if err != nil {
|
||||
log.Printf("Error fetching auth providers: %v", err)
|
||||
c.String(http.StatusInternalServerError, "")
|
||||
return
|
||||
}
|
||||
|
||||
components.AuthProviderButtons(providers).Render(c.Request.Context(), c.Writer)
|
||||
}
|
||||
|
||||
// HandleAuthProviderInit initiates authentication with the selected provider
|
||||
func (h *Handlers) HandleAuthProviderInit(c *gin.Context) {
|
||||
// Get provider ID
|
||||
providerID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
h.HandleBadRequest(c, "Invalid Provider ID", "The provider ID is not valid")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the auth provider
|
||||
provider, err := h.DB.GetAuthProviderByID(c.Request.Context(), uint(providerID))
|
||||
if err != nil || !provider.Enabled {
|
||||
h.HandleBadRequest(c, "Provider Not Available", "The authentication provider is not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate state parameter for CSRF protection
|
||||
state, err := generateResetToken(32)
|
||||
if err != nil {
|
||||
h.HandleServerError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Store state in session/cookie for validation on callback
|
||||
c.SetCookie("auth_state", state, 3600, "/", "", false, true)
|
||||
c.SetCookie("auth_provider_id", fmt.Sprintf("%d", providerID), 3600, "/", "", false, true)
|
||||
|
||||
// Get base URL for redirect URI
|
||||
baseURL := os.Getenv("BASE_URL")
|
||||
if baseURL == "" {
|
||||
// Try to detect the base URL from the request
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
baseURL = fmt.Sprintf("%s://%s", scheme, c.Request.Host)
|
||||
}
|
||||
|
||||
// Default redirect URL if not specified in provider
|
||||
redirectURI := provider.RedirectURL
|
||||
if redirectURI == "" {
|
||||
redirectURI = fmt.Sprintf("%s/auth/callback", baseURL)
|
||||
}
|
||||
|
||||
// Handle different provider types
|
||||
switch provider.Type {
|
||||
case db.ProviderTypeOIDC, db.ProviderTypeOAuth2:
|
||||
// Build the authorization URL for OIDC/OAuth2
|
||||
scopes := "openid profile email"
|
||||
if provider.Scopes != "" {
|
||||
scopes = provider.Scopes
|
||||
}
|
||||
|
||||
// Get config for OIDC
|
||||
configData, _ := provider.GetConfig()
|
||||
var authEndpoint string
|
||||
|
||||
if provider.Type == db.ProviderTypeOIDC && configData["discovery_url"] != "" {
|
||||
// Fetch from discovery endpoint
|
||||
discoveryURL, _ := configData["discovery_url"].(string)
|
||||
discoveryData, err := h.fetchOIDCDiscovery(discoveryURL)
|
||||
if err != nil {
|
||||
h.HandleServerError(c, err)
|
||||
return
|
||||
}
|
||||
authEndpoint = discoveryData["authorization_endpoint"].(string)
|
||||
} else {
|
||||
// Use provider URL as base
|
||||
authEndpoint = fmt.Sprintf("%s/oauth2/authorize", provider.ProviderURL)
|
||||
}
|
||||
|
||||
// Build the auth URL
|
||||
authURL := fmt.Sprintf("%s?client_id=%s&redirect_uri=%s&scope=%s&response_type=code&state=%s",
|
||||
authEndpoint,
|
||||
url.QueryEscape(provider.ClientID),
|
||||
url.QueryEscape(redirectURI),
|
||||
url.QueryEscape(scopes),
|
||||
url.QueryEscape(state))
|
||||
|
||||
// Redirect user to the authorization endpoint
|
||||
c.Redirect(http.StatusFound, authURL)
|
||||
return
|
||||
|
||||
case db.ProviderTypeAuthentik:
|
||||
// Build the authorization URL for Authentik
|
||||
configData, _ := provider.GetConfig()
|
||||
tenant := "default"
|
||||
if tenantID, ok := configData["tenant_id"].(string); ok && tenantID != "" {
|
||||
tenant = tenantID
|
||||
}
|
||||
|
||||
// Construct Authentik authorization URL
|
||||
scopes := "openid profile email"
|
||||
if provider.Scopes != "" {
|
||||
scopes = provider.Scopes
|
||||
}
|
||||
|
||||
authURL := fmt.Sprintf("%s/application/o/authorize/?client_id=%s&redirect_uri=%s&scope=%s&response_type=code&state=%s&tenant=%s",
|
||||
strings.TrimSuffix(provider.ProviderURL, "/"),
|
||||
url.QueryEscape(provider.ClientID),
|
||||
url.QueryEscape(redirectURI),
|
||||
url.QueryEscape(scopes),
|
||||
url.QueryEscape(state),
|
||||
url.QueryEscape(tenant))
|
||||
|
||||
// Redirect user to the Authentik authorization endpoint
|
||||
c.Redirect(http.StatusFound, authURL)
|
||||
return
|
||||
|
||||
case db.ProviderTypeSAML:
|
||||
// Note: SAML flows work differently than OAuth2/OIDC
|
||||
// Here you would typically generate a SAML request and redirect the user
|
||||
// This is just a placeholder - actual SAML implementation would need a SAML library
|
||||
h.HandleBadRequest(c, "SAML Not Implemented", "SAML authentication is not yet implemented")
|
||||
return
|
||||
|
||||
default:
|
||||
h.HandleBadRequest(c, "Unsupported Provider", "The authentication provider type is not supported")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to fetch OIDC discovery document
|
||||
func (h *Handlers) fetchOIDCDiscovery(discoveryURL string) (map[string]interface{}, error) {
|
||||
resp, err := http.Get(discoveryURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to fetch discovery document, status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// HandleAuthProviderCallback handles the callback from external authentication providers
|
||||
func (h *Handlers) HandleAuthProviderCallback(c *gin.Context) {
|
||||
// Get state and code from query params
|
||||
state := c.Query("state")
|
||||
code := c.Query("code")
|
||||
|
||||
if state == "" || code == "" {
|
||||
h.HandleBadRequest(c, "Invalid Authentication Response", "Missing required parameters from authentication provider")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify state to prevent CSRF
|
||||
storedState, err := c.Cookie("auth_state")
|
||||
if err != nil || state != storedState {
|
||||
h.HandleBadRequest(c, "Invalid Authentication State", "The authentication process was corrupted or expired")
|
||||
return
|
||||
}
|
||||
|
||||
// Get provider ID from cookie
|
||||
providerIDStr, err := c.Cookie("auth_provider_id")
|
||||
if err != nil {
|
||||
h.HandleBadRequest(c, "Authentication Error", "Unable to determine authentication provider")
|
||||
return
|
||||
}
|
||||
|
||||
providerID, err := strconv.ParseUint(providerIDStr, 10, 64)
|
||||
if err != nil {
|
||||
h.HandleBadRequest(c, "Invalid Provider ID", "The provider ID is not valid")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the auth provider
|
||||
provider, err := h.DB.GetAuthProviderByID(c.Request.Context(), uint(providerID))
|
||||
if err != nil || !provider.Enabled {
|
||||
h.HandleBadRequest(c, "Provider Not Available", "The authentication provider is not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Get base URL for redirect URI
|
||||
baseURL := os.Getenv("BASE_URL")
|
||||
if baseURL == "" {
|
||||
// Try to detect the base URL from the request
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
baseURL = fmt.Sprintf("%s://%s", scheme, c.Request.Host)
|
||||
}
|
||||
|
||||
// Use provider's redirect URL or default
|
||||
redirectURI := provider.RedirectURL
|
||||
if redirectURI == "" {
|
||||
redirectURI = fmt.Sprintf("%s/auth/callback", baseURL)
|
||||
}
|
||||
|
||||
// Exchange code for tokens
|
||||
var userInfo map[string]interface{}
|
||||
var externalID string
|
||||
var email string
|
||||
var username string
|
||||
var displayName string
|
||||
|
||||
switch provider.Type {
|
||||
case db.ProviderTypeOIDC, db.ProviderTypeOAuth2, db.ProviderTypeAuthentik:
|
||||
// Get token endpoint
|
||||
var tokenEndpoint string
|
||||
if provider.Type == db.ProviderTypeOIDC {
|
||||
configData, _ := provider.GetConfig()
|
||||
if discoveryURL, ok := configData["discovery_url"].(string); ok && discoveryURL != "" {
|
||||
discoveryData, err := h.fetchOIDCDiscovery(discoveryURL)
|
||||
if err != nil {
|
||||
h.HandleServerError(c, err)
|
||||
return
|
||||
}
|
||||
tokenEndpoint = discoveryData["token_endpoint"].(string)
|
||||
} else {
|
||||
tokenEndpoint = fmt.Sprintf("%s/oauth2/token", provider.ProviderURL)
|
||||
}
|
||||
} else if provider.Type == db.ProviderTypeAuthentik {
|
||||
tokenEndpoint = fmt.Sprintf("%s/application/o/token/", strings.TrimSuffix(provider.ProviderURL, "/"))
|
||||
} else {
|
||||
tokenEndpoint = fmt.Sprintf("%s/oauth/token", provider.ProviderURL)
|
||||
}
|
||||
|
||||
// Exchange code for token
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", "authorization_code")
|
||||
data.Set("code", code)
|
||||
data.Set("redirect_uri", redirectURI)
|
||||
data.Set("client_id", provider.ClientID)
|
||||
data.Set("client_secret", provider.ClientSecret)
|
||||
|
||||
resp, err := http.PostForm(tokenEndpoint, data)
|
||||
if err != nil {
|
||||
h.HandleServerError(c, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
log.Printf("Error exchanging code for token: %s", string(body))
|
||||
h.HandleBadRequest(c, "Authentication Failed", "Failed to authenticate with the provider")
|
||||
return
|
||||
}
|
||||
|
||||
var tokenResponse map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
|
||||
h.HandleServerError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get access token
|
||||
accessToken, ok := tokenResponse["access_token"].(string)
|
||||
if !ok {
|
||||
h.HandleBadRequest(c, "Authentication Failed", "Invalid token response from provider")
|
||||
return
|
||||
}
|
||||
|
||||
// Get user info
|
||||
userInfo, err = h.fetchUserInfo(provider, accessToken)
|
||||
if err != nil {
|
||||
h.HandleServerError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract fields based on attribute mapping
|
||||
var attributeMapping map[string]string
|
||||
if provider.AttributeMapping != "" {
|
||||
if err := json.Unmarshal([]byte(provider.AttributeMapping), &attributeMapping); err != nil {
|
||||
log.Printf("Error parsing attribute mapping: %v", err)
|
||||
// Use defaults if mapping fails
|
||||
attributeMapping = map[string]string{
|
||||
"username": "preferred_username",
|
||||
"email": "email",
|
||||
"name": "name",
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Default mapping
|
||||
attributeMapping = map[string]string{
|
||||
"username": "preferred_username",
|
||||
"email": "email",
|
||||
"name": "name",
|
||||
}
|
||||
}
|
||||
|
||||
// Extract user info using the attribute mapping
|
||||
if subValue, ok := userInfo["sub"].(string); ok {
|
||||
externalID = subValue
|
||||
} else {
|
||||
// Generate fallback ID if 'sub' is not available
|
||||
externalID = fmt.Sprintf("%s_%d", provider.Type, time.Now().Unix())
|
||||
}
|
||||
|
||||
// Extract email - this is critical for user matching
|
||||
if emailAttr, ok := attributeMapping["email"]; ok && emailAttr != "" {
|
||||
if emailValue, ok := userInfo[emailAttr].(string); ok {
|
||||
email = emailValue
|
||||
}
|
||||
}
|
||||
if email == "" && userInfo["email"] != nil {
|
||||
email = userInfo["email"].(string)
|
||||
}
|
||||
|
||||
// Extract username
|
||||
if usernameAttr, ok := attributeMapping["username"]; ok && usernameAttr != "" {
|
||||
if usernameValue, ok := userInfo[usernameAttr].(string); ok {
|
||||
username = usernameValue
|
||||
}
|
||||
}
|
||||
if username == "" && userInfo["preferred_username"] != nil {
|
||||
username = userInfo["preferred_username"].(string)
|
||||
}
|
||||
|
||||
// Extract display name
|
||||
if nameAttr, ok := attributeMapping["name"]; ok && nameAttr != "" {
|
||||
if nameValue, ok := userInfo[nameAttr].(string); ok {
|
||||
displayName = nameValue
|
||||
}
|
||||
}
|
||||
if displayName == "" && userInfo["name"] != nil {
|
||||
displayName = userInfo["name"].(string)
|
||||
}
|
||||
|
||||
default:
|
||||
h.HandleBadRequest(c, "Unsupported Provider", "The authentication provider type is not supported")
|
||||
return
|
||||
}
|
||||
|
||||
// Require email for user identification
|
||||
if email == "" {
|
||||
h.HandleBadRequest(c, "Authentication Failed", "Unable to retrieve email address from the provider")
|
||||
return
|
||||
}
|
||||
|
||||
// Look up existing user by email
|
||||
existingUser, err := h.DB.GetUserByEmail(email)
|
||||
if err != nil {
|
||||
// If user doesn't exist, check if auto-provisioning is allowed
|
||||
// For now, we'll require existing users
|
||||
h.HandleBadRequest(c, "Authentication Failed", "No account exists with this email address")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for existing identity
|
||||
identity, err := h.DB.GetExternalUserIdentity(c.Request.Context(), provider.ID, externalID)
|
||||
if err != nil || identity == nil {
|
||||
// If identity doesn't exist, create it
|
||||
identity = &db.ExternalUserIdentity{
|
||||
UserID: existingUser.ID,
|
||||
ProviderID: provider.ID,
|
||||
ProviderType: provider.Type,
|
||||
ExternalID: externalID,
|
||||
Email: email,
|
||||
Username: username,
|
||||
DisplayName: displayName,
|
||||
LastLogin: sql.NullTime{Time: time.Now(), Valid: true},
|
||||
}
|
||||
|
||||
// Store provider data
|
||||
if err := identity.SetProviderData(userInfo); err != nil {
|
||||
log.Printf("Error serializing provider data: %v", err)
|
||||
}
|
||||
|
||||
// Extract and store groups if available
|
||||
var attributeMapping map[string]string
|
||||
if provider.AttributeMapping != "" {
|
||||
if err := json.Unmarshal([]byte(provider.AttributeMapping), &attributeMapping); err == nil {
|
||||
if groupsAttr, ok := attributeMapping["groups"]; ok && groupsAttr != "" {
|
||||
if groupsValue, ok := userInfo[groupsAttr]; ok {
|
||||
// Handle different group formats (array or comma-separated string)
|
||||
var groups []string
|
||||
switch v := groupsValue.(type) {
|
||||
case []interface{}:
|
||||
for _, g := range v {
|
||||
if gs, ok := g.(string); ok {
|
||||
groups = append(groups, gs)
|
||||
}
|
||||
}
|
||||
case string:
|
||||
groups = strings.Split(v, ",")
|
||||
}
|
||||
|
||||
if len(groups) > 0 {
|
||||
if err := identity.SetGroups(groups); err != nil {
|
||||
log.Printf("Error serializing groups: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("Error parsing attribute mapping: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the identity
|
||||
if err := h.DB.CreateExternalUserIdentity(c.Request.Context(), identity); err != nil {
|
||||
log.Printf("Error creating external identity: %v", err)
|
||||
h.HandleServerError(c, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Update existing identity
|
||||
identity.LastLogin = sql.NullTime{Time: time.Now(), Valid: true}
|
||||
identity.Email = email
|
||||
identity.Username = username
|
||||
identity.DisplayName = displayName
|
||||
|
||||
// Update provider data
|
||||
if err := identity.SetProviderData(userInfo); err != nil {
|
||||
log.Printf("Error serializing provider data: %v", err)
|
||||
}
|
||||
|
||||
// Save the updated identity
|
||||
if err := h.DB.UpdateExternalUserIdentity(c.Request.Context(), identity); err != nil {
|
||||
log.Printf("Error updating external identity: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update provider usage stats
|
||||
provider.SuccessfulLogins++
|
||||
provider.LastUsed = sql.NullTime{Time: time.Now(), Valid: true}
|
||||
if err := h.DB.UpdateAuthProvider(c.Request.Context(), provider); err != nil {
|
||||
log.Printf("Error updating provider stats: %v", err)
|
||||
}
|
||||
|
||||
// Create session for the user
|
||||
isAdmin := false
|
||||
if existingUser.IsAdmin != nil {
|
||||
isAdmin = *existingUser.IsAdmin
|
||||
}
|
||||
token, err := h.GenerateJWT(existingUser.ID, existingUser.Email, isAdmin)
|
||||
if err != nil {
|
||||
h.HandleServerError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Clear auth cookies
|
||||
c.SetCookie("auth_state", "", -1, "/", "", false, true)
|
||||
c.SetCookie("auth_provider_id", "", -1, "/", "", false, true)
|
||||
|
||||
// Set session cookie
|
||||
c.SetCookie("jwt_token", token, 86400, "/", "", false, true)
|
||||
|
||||
// Redirect to dashboard
|
||||
c.Redirect(http.StatusFound, "/dashboard")
|
||||
}
|
||||
|
||||
// Helper function to fetch user info with access token
|
||||
func (h *Handlers) fetchUserInfo(provider *db.AuthProvider, accessToken string) (map[string]interface{}, error) {
|
||||
var userInfoEndpoint string
|
||||
|
||||
// Determine user info endpoint based on provider type
|
||||
switch provider.Type {
|
||||
case db.ProviderTypeOIDC:
|
||||
// For OIDC, check if we have discovery URL
|
||||
configData, _ := provider.GetConfig()
|
||||
if discoveryURL, ok := configData["discovery_url"].(string); ok && discoveryURL != "" {
|
||||
discoveryData, err := h.fetchOIDCDiscovery(discoveryURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userInfoEndpoint = discoveryData["userinfo_endpoint"].(string)
|
||||
} else {
|
||||
userInfoEndpoint = fmt.Sprintf("%s/oauth2/userinfo", provider.ProviderURL)
|
||||
}
|
||||
|
||||
case db.ProviderTypeAuthentik:
|
||||
userInfoEndpoint = fmt.Sprintf("%s/application/o/userinfo/", strings.TrimSuffix(provider.ProviderURL, "/"))
|
||||
|
||||
case db.ProviderTypeOAuth2:
|
||||
userInfoEndpoint = fmt.Sprintf("%s/oauth/userinfo", provider.ProviderURL)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported provider type: %s", provider.Type)
|
||||
}
|
||||
|
||||
// Make request to user info endpoint
|
||||
req, err := http.NewRequest("GET", userInfoEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add authorization header
|
||||
req.Header.Add("Authorization", "Bearer "+accessToken)
|
||||
|
||||
// Make the request
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("failed to get user info: %s", string(body))
|
||||
}
|
||||
|
||||
// Parse response
|
||||
var userInfo map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
package handlers
|
||||
|
||||
// Authentication Provider handlers for the GoMFT application
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
)
|
||||
|
||||
// AuthProvidersPage renders the page listing all authentication providers
|
||||
func (h *Handlers) AuthProvidersPage(c *gin.Context) {
|
||||
// Get user from context
|
||||
userID := c.GetUint("userID")
|
||||
if userID == 0 {
|
||||
h.HandleUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Get all auth providers
|
||||
providers, err := h.DB.GetAllAuthProviders(c.Request.Context())
|
||||
if err != nil {
|
||||
h.HandleServerError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
components.AuthProviders(c.Request.Context(), providers).Render(c, c.Writer)
|
||||
|
||||
}
|
||||
|
||||
// NewAuthProviderPage renders the form to create a new authentication provider
|
||||
func (h *Handlers) NewAuthProviderPage(c *gin.Context) {
|
||||
// Get user from context
|
||||
userID := c.GetUint("userID")
|
||||
if userID == 0 {
|
||||
h.HandleUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Create an empty auth provider for the form
|
||||
provider := &db.AuthProvider{}
|
||||
|
||||
// Render the auth provider form for creating a new provider
|
||||
components.AuthProviderForm(c.Request.Context(), provider, true).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// EditAuthProviderPage renders the form to edit an existing authentication provider
|
||||
func (h *Handlers) EditAuthProviderPage(c *gin.Context) {
|
||||
// Get user from context
|
||||
userID := c.GetUint("userID")
|
||||
if userID == 0 {
|
||||
h.HandleUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
providerID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
h.HandleBadRequest(c, "Invalid Provider ID", "The provider ID is not valid")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the auth provider
|
||||
provider, err := h.DB.GetAuthProviderByID(c.Request.Context(), uint(providerID))
|
||||
if err != nil {
|
||||
h.HandleBadRequest(c, "Provider Not Found", "The authentication provider could not be found")
|
||||
return
|
||||
}
|
||||
|
||||
// Render the auth provider form for editing
|
||||
components.AuthProviderForm(c.Request.Context(), provider, false).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleCreateAuthProvider handles the form submission to create a new authentication provider
|
||||
func (h *Handlers) HandleCreateAuthProvider(c *gin.Context) {
|
||||
// Get user from context
|
||||
userID := c.GetUint("userID")
|
||||
if userID == 0 {
|
||||
h.HandleUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.Request.ParseForm(); err != nil {
|
||||
h.HandleBadRequest(c, "Invalid Form Data", "Could not parse form data")
|
||||
return
|
||||
}
|
||||
|
||||
provider := &db.AuthProvider{
|
||||
Name: c.PostForm("name"),
|
||||
Type: db.ProviderType(c.PostForm("type")),
|
||||
ProviderURL: c.PostForm("provider_url"),
|
||||
ClientID: c.PostForm("client_id"),
|
||||
ClientSecret: c.PostForm("client_secret"),
|
||||
RedirectURL: c.PostForm("redirect_url"),
|
||||
Scopes: c.PostForm("scopes"),
|
||||
Description: c.PostForm("description"),
|
||||
IconURL: c.PostForm("icon_url"),
|
||||
Enabled: c.PostForm("enabled") == "on",
|
||||
}
|
||||
|
||||
// Process config values based on provider type
|
||||
config := make(map[string]interface{})
|
||||
switch provider.Type {
|
||||
case db.ProviderTypeAuthentik:
|
||||
if tenant := c.PostForm("authentik_tenant"); tenant != "" {
|
||||
config["tenant_id"] = tenant
|
||||
}
|
||||
case db.ProviderTypeOIDC:
|
||||
if discoveryURL := c.PostForm("oidc_discovery_url"); discoveryURL != "" {
|
||||
config["discovery_url"] = discoveryURL
|
||||
}
|
||||
case db.ProviderTypeSAML:
|
||||
if metadataURL := c.PostForm("saml_metadata_url"); metadataURL != "" {
|
||||
config["metadata_url"] = metadataURL
|
||||
}
|
||||
}
|
||||
|
||||
// Set the config
|
||||
if len(config) > 0 {
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
h.HandleBadRequest(c, "Invalid Configuration", "Could not process provider configuration")
|
||||
return
|
||||
}
|
||||
provider.Config = string(configJSON)
|
||||
}
|
||||
|
||||
// Process attribute mappings
|
||||
attrMapping := map[string]string{
|
||||
"username": c.PostForm("attr_username"),
|
||||
"email": c.PostForm("attr_email"),
|
||||
"name": c.PostForm("attr_name"),
|
||||
"groups": c.PostForm("attr_groups"),
|
||||
}
|
||||
|
||||
// Remove empty mappings
|
||||
for k, v := range attrMapping {
|
||||
if v == "" {
|
||||
delete(attrMapping, k)
|
||||
}
|
||||
}
|
||||
|
||||
if len(attrMapping) > 0 {
|
||||
mappingJSON, err := json.Marshal(attrMapping)
|
||||
if err != nil {
|
||||
h.HandleBadRequest(c, "Invalid Attribute Mapping", "Could not process attribute mappings")
|
||||
return
|
||||
}
|
||||
provider.AttributeMapping = string(mappingJSON)
|
||||
}
|
||||
|
||||
// Create the provider
|
||||
if err := h.DB.CreateAuthProvider(c.Request.Context(), provider); err != nil {
|
||||
h.HandleServerError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[Audit] User %d created auth provider %d of type %s with name '%s'",
|
||||
userID, provider.ID, provider.Type, provider.Name)
|
||||
|
||||
// Set success flash and redirect
|
||||
c.SetCookie("flash_message", fmt.Sprintf("Authentication provider '%s' created successfully", provider.Name),
|
||||
3600, "/", "", false, true)
|
||||
c.SetCookie("flash_type", "success", 3600, "/", "", false, true)
|
||||
|
||||
c.Redirect(http.StatusFound, "/admin/settings/auth-providers")
|
||||
}
|
||||
|
||||
// HandleUpdateAuthProvider handles the form submission to update an existing authentication provider
|
||||
func (h *Handlers) HandleUpdateAuthProvider(c *gin.Context) {
|
||||
// Get user from context
|
||||
userID := c.GetUint("userID")
|
||||
if userID == 0 {
|
||||
h.HandleUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
providerID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
h.HandleBadRequest(c, "Invalid Provider ID", "The provider ID is not valid")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the existing provider
|
||||
existingProvider, err := h.DB.GetAuthProviderByID(c.Request.Context(), uint(providerID))
|
||||
if err != nil {
|
||||
h.HandleBadRequest(c, "Provider Not Found", "The authentication provider could not be found")
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.Request.ParseForm(); err != nil {
|
||||
h.HandleBadRequest(c, "Invalid Form Data", "Could not parse form data")
|
||||
return
|
||||
}
|
||||
|
||||
// Update the provider with form data
|
||||
existingProvider.Name = c.PostForm("name")
|
||||
existingProvider.ProviderURL = c.PostForm("provider_url")
|
||||
existingProvider.ClientID = c.PostForm("client_id")
|
||||
existingProvider.IconURL = c.PostForm("icon_url")
|
||||
|
||||
// Only update client secret if provided
|
||||
if clientSecret := c.PostForm("client_secret"); clientSecret != "" {
|
||||
existingProvider.ClientSecret = clientSecret
|
||||
}
|
||||
|
||||
existingProvider.RedirectURL = c.PostForm("redirect_url")
|
||||
existingProvider.Scopes = c.PostForm("scopes")
|
||||
existingProvider.Description = c.PostForm("description")
|
||||
existingProvider.Enabled = c.PostForm("enabled") == "on"
|
||||
|
||||
// Update the type if changed
|
||||
if providerType := c.PostForm("type"); providerType != "" {
|
||||
existingProvider.Type = db.ProviderType(providerType)
|
||||
}
|
||||
|
||||
// Process config values based on provider type
|
||||
config := make(map[string]interface{})
|
||||
switch existingProvider.Type {
|
||||
case db.ProviderTypeAuthentik:
|
||||
if tenant := c.PostForm("authentik_tenant"); tenant != "" {
|
||||
config["tenant_id"] = tenant
|
||||
}
|
||||
case db.ProviderTypeOIDC:
|
||||
if discoveryURL := c.PostForm("oidc_discovery_url"); discoveryURL != "" {
|
||||
config["discovery_url"] = discoveryURL
|
||||
}
|
||||
case db.ProviderTypeSAML:
|
||||
if metadataURL := c.PostForm("saml_metadata_url"); metadataURL != "" {
|
||||
config["metadata_url"] = metadataURL
|
||||
}
|
||||
}
|
||||
|
||||
// Set the config
|
||||
if len(config) > 0 {
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
h.HandleBadRequest(c, "Invalid Configuration", "Could not process provider configuration")
|
||||
return
|
||||
}
|
||||
existingProvider.Config = string(configJSON)
|
||||
}
|
||||
|
||||
// Process attribute mappings
|
||||
attrMapping := map[string]string{
|
||||
"username": c.PostForm("attr_username"),
|
||||
"email": c.PostForm("attr_email"),
|
||||
"name": c.PostForm("attr_name"),
|
||||
"groups": c.PostForm("attr_groups"),
|
||||
}
|
||||
|
||||
// Remove empty mappings
|
||||
for k, v := range attrMapping {
|
||||
if v == "" {
|
||||
delete(attrMapping, k)
|
||||
}
|
||||
}
|
||||
|
||||
if len(attrMapping) > 0 {
|
||||
mappingJSON, err := json.Marshal(attrMapping)
|
||||
if err != nil {
|
||||
h.HandleBadRequest(c, "Invalid Attribute Mapping", "Could not process attribute mappings")
|
||||
return
|
||||
}
|
||||
existingProvider.AttributeMapping = string(mappingJSON)
|
||||
}
|
||||
|
||||
// Update the provider
|
||||
if err := h.DB.UpdateAuthProvider(c.Request.Context(), existingProvider); err != nil {
|
||||
h.HandleServerError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[Audit] User %d updated auth provider %d of type %s with name '%s'",
|
||||
userID, existingProvider.ID, existingProvider.Type, existingProvider.Name)
|
||||
|
||||
// Set success flash and redirect
|
||||
c.SetCookie("flash_message", fmt.Sprintf("Authentication provider '%s' updated successfully", existingProvider.Name),
|
||||
3600, "/", "", false, true)
|
||||
c.SetCookie("flash_type", "success", 3600, "/", "", false, true)
|
||||
|
||||
c.Redirect(http.StatusFound, "/admin/settings/auth-providers")
|
||||
}
|
||||
|
||||
// HandleDeleteAuthProvider handles the request to delete an authentication provider
|
||||
func (h *Handlers) HandleDeleteAuthProvider(c *gin.Context) {
|
||||
// Get user from context
|
||||
userID := c.GetUint("userID")
|
||||
if userID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Unauthorized",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
providerID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Invalid provider ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Get the provider to be deleted
|
||||
provider, err := h.DB.GetAuthProviderByID(c.Request.Context(), uint(providerID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "Authentication provider not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if there are any user identities associated with this provider
|
||||
count, err := h.DB.CountExternalUserIdentitiesByProviderID(c.Request.Context(), uint(providerID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to check if provider is in use",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Cannot delete provider '%s' because it has %d associated user identities", provider.Name, count),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the provider
|
||||
if err := h.DB.DeleteAuthProvider(c.Request.Context(), uint(providerID)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to delete authentication provider",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[Audit] User %d deleted auth provider %d of type %s with name '%s'",
|
||||
userID, provider.ID, provider.Type, provider.Name)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf("Authentication provider '%s' deleted successfully", provider.Name),
|
||||
})
|
||||
}
|
||||
|
||||
// HandleTestAuthProviderConnection tests the connection to an authentication provider
|
||||
func (h *Handlers) HandleTestAuthProviderConnection(c *gin.Context) {
|
||||
// Get user from context
|
||||
userID := c.GetUint("userID")
|
||||
if userID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Unauthorized",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
providerID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Invalid provider ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Get the provider
|
||||
provider, err := h.DB.GetAuthProviderByID(c.Request.Context(), uint(providerID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "Authentication provider not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt to test the connection based on provider type
|
||||
var testResult error
|
||||
switch provider.Type {
|
||||
case db.ProviderTypeAuthentik:
|
||||
testResult = h.testAuthentikConnection(provider)
|
||||
case db.ProviderTypeOIDC:
|
||||
testResult = h.testOIDCConnection(provider)
|
||||
case db.ProviderTypeSAML:
|
||||
testResult = h.testSAMLConnection(provider)
|
||||
case db.ProviderTypeOAuth2:
|
||||
testResult = h.testOAuth2Connection(provider)
|
||||
default:
|
||||
testResult = fmt.Errorf("unsupported provider type: %s", provider.Type)
|
||||
}
|
||||
|
||||
if testResult != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Connection test failed: %s", testResult.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[Audit] User %d successfully tested connection to auth provider %d of type %s",
|
||||
userID, provider.ID, provider.Type)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Connection test successful",
|
||||
})
|
||||
}
|
||||
|
||||
// Test methods for different provider types
|
||||
func (h *Handlers) testAuthentikConnection(provider *db.AuthProvider) error {
|
||||
// TODO: Implement actual test logic for Authentik
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handlers) testOIDCConnection(provider *db.AuthProvider) error {
|
||||
// TODO: Implement actual test logic for OIDC
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handlers) testSAMLConnection(provider *db.AuthProvider) error {
|
||||
// TODO: Implement actual test logic for SAML
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handlers) testOAuth2Connection(provider *db.AuthProvider) error {
|
||||
// TODO: Implement actual test logic for OAuth2
|
||||
return nil
|
||||
}
|
||||
@@ -21,6 +21,11 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
||||
router.GET("/reset-password", h.HandleResetPasswordPage)
|
||||
router.POST("/reset-password", h.HandleResetPassword)
|
||||
|
||||
// External Authentication Provider routes for login page
|
||||
router.GET("/auth/providers", h.GetAuthProviders)
|
||||
router.GET("/auth/provider/:id", h.HandleAuthProviderInit)
|
||||
router.GET("/auth/callback", h.HandleAuthProviderCallback)
|
||||
|
||||
// Protected routes
|
||||
authorized := router.Group("/")
|
||||
authorized.Use(h.AuthMiddleware())
|
||||
@@ -145,9 +150,26 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
||||
settingsGroup.Use(h.PermissionMiddleware("system.settings"))
|
||||
{
|
||||
settingsGroup.GET("", h.HandleSettings)
|
||||
// settingsGroup.GET("/backups", h.HandleBackupsPage)
|
||||
// settingsGroup.POST("/backups", h.HandleCreateBackup)
|
||||
// settingsGroup.GET("/logs", h.HandleLogsPage)
|
||||
// settingsGroup.POST("/logs/download", h.HandleDownloadLogs)
|
||||
// settingsGroup.DELETE("/logs", h.HandlePurgeLogs)
|
||||
|
||||
// Auth Provider routes
|
||||
authProviderGroup := settingsGroup.Group("/auth-providers")
|
||||
authProviderGroup.GET("", h.AuthProvidersPage)
|
||||
authProviderGroup.GET("/new", h.NewAuthProviderPage)
|
||||
authProviderGroup.POST("", h.HandleCreateAuthProvider)
|
||||
authProviderGroup.GET("/:id/edit", h.EditAuthProviderPage)
|
||||
authProviderGroup.POST("/:id", h.HandleUpdateAuthProvider)
|
||||
authProviderGroup.DELETE("/:id", h.HandleDeleteAuthProvider)
|
||||
authProviderGroup.POST("/:id/test", h.HandleTestAuthProviderConnection)
|
||||
|
||||
settingsGroup.POST("/notifications", h.HandleCreateNotificationService)
|
||||
settingsGroup.DELETE("/notifications/:id", h.HandleDeleteNotificationService)
|
||||
settingsGroup.POST("/notifications/test", h.HandleTestNotification)
|
||||
|
||||
settingsGroup.POST("/general", h.HandleSettings) // Placeholder for future implementation
|
||||
settingsGroup.POST("/security", h.HandleSettings) // Placeholder for future implementation
|
||||
}
|
||||
@@ -209,6 +231,14 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
||||
apiAdmin.POST("/users", h.HandleAPICreateUser)
|
||||
apiAdmin.PUT("/users/:id", h.HandleAPIUpdateUser)
|
||||
apiAdmin.DELETE("/users/:id", h.HandleAPIDeleteUser)
|
||||
|
||||
// Auth providers API routes
|
||||
apiAdmin.GET("/auth-providers", h.HandleAPIUsers) // Placeholder for now
|
||||
apiAdmin.GET("/auth-providers/:id", h.HandleAPIUser) // Placeholder for now
|
||||
apiAdmin.POST("/auth-providers", h.HandleAPICreateUser) // Placeholder for now
|
||||
apiAdmin.PUT("/auth-providers/:id", h.HandleAPIUpdateUser) // Placeholder for now
|
||||
apiAdmin.DELETE("/auth-providers/:id", h.HandleAPIDeleteUser) // Placeholder for now
|
||||
apiAdmin.POST("/auth-providers/:id/test", h.HandleAPIUser) // Placeholder for now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user