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:
@@ -0,0 +1,154 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ProviderType represents the type of authentication provider
|
||||
type ProviderType string
|
||||
|
||||
const (
|
||||
// ProviderTypeAuthentik represents an Authentik authentication provider
|
||||
ProviderTypeAuthentik ProviderType = "authentik"
|
||||
|
||||
// ProviderTypeOIDC represents an OpenID Connect authentication provider
|
||||
ProviderTypeOIDC ProviderType = "oidc"
|
||||
|
||||
// ProviderTypeSAML represents a SAML authentication provider
|
||||
ProviderTypeSAML ProviderType = "saml"
|
||||
|
||||
// ProviderTypeOAuth2 represents an OAuth2 authentication provider
|
||||
ProviderTypeOAuth2 ProviderType = "oauth2"
|
||||
)
|
||||
|
||||
// AuthProvider represents an external authentication provider configuration
|
||||
type AuthProvider struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Type ProviderType `gorm:"not null" json:"type"`
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
Description string `json:"description"`
|
||||
ProviderURL string `json:"provider_url"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"-"` // Not returned in JSON responses
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
Scopes string `json:"scopes"`
|
||||
AttributeMapping string `json:"attribute_mapping"`
|
||||
Config string `json:"-"` // Stores provider-specific configuration
|
||||
IconURL string `json:"icon_url"` // URL to provider icon
|
||||
SuccessfulLogins int `json:"successful_logins"`
|
||||
LastUsed sql.NullTime `json:"last_used"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Unmarshalled config
|
||||
configData map[string]interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// GetConfig returns the unmarshalled configuration data
|
||||
func (p *AuthProvider) GetConfig() (map[string]interface{}, error) {
|
||||
if p.configData == nil && p.Config != "" {
|
||||
err := json.Unmarshal([]byte(p.Config), &p.configData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if p.configData == nil {
|
||||
p.configData = make(map[string]interface{})
|
||||
}
|
||||
|
||||
return p.configData, nil
|
||||
}
|
||||
|
||||
// SetConfig sets the configuration data and marshals it to JSON
|
||||
func (p *AuthProvider) SetConfig(data map[string]interface{}) error {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Config = string(jsonData)
|
||||
p.configData = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExternalUserIdentity represents a user identity from an external authentication provider
|
||||
type ExternalUserIdentity struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
UserID uint `gorm:"not null" json:"user_id"`
|
||||
ProviderID uint `gorm:"not null" json:"provider_id"`
|
||||
ProviderType ProviderType `gorm:"not null" json:"provider_type"`
|
||||
ExternalID string `gorm:"not null" json:"external_id"`
|
||||
Email string `gorm:"not null" json:"email"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Groups string `json:"groups"` // JSON array of groups
|
||||
LastLogin sql.NullTime `json:"last_login"`
|
||||
ProviderData string `json:"-"` // Raw data from provider
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Foreign key relationships
|
||||
User User `gorm:"foreignKey:UserID" json:"-"`
|
||||
Provider AuthProvider `gorm:"foreignKey:ProviderID" json:"-"`
|
||||
|
||||
// Unmarshalled provider data
|
||||
providerDataObj map[string]interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// GetProviderData returns the unmarshalled provider data
|
||||
func (e *ExternalUserIdentity) GetProviderData() (map[string]interface{}, error) {
|
||||
if e.providerDataObj == nil && e.ProviderData != "" {
|
||||
err := json.Unmarshal([]byte(e.ProviderData), &e.providerDataObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if e.providerDataObj == nil {
|
||||
e.providerDataObj = make(map[string]interface{})
|
||||
}
|
||||
|
||||
return e.providerDataObj, nil
|
||||
}
|
||||
|
||||
// SetProviderData sets the provider data and marshals it to JSON
|
||||
func (e *ExternalUserIdentity) SetProviderData(data map[string]interface{}) error {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.ProviderData = string(jsonData)
|
||||
e.providerDataObj = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGroups returns the unmarshalled groups
|
||||
func (e *ExternalUserIdentity) GetGroups() ([]string, error) {
|
||||
var groups []string
|
||||
if e.Groups == "" {
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(e.Groups), &groups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// SetGroups sets the groups and marshals them to JSON
|
||||
func (e *ExternalUserIdentity) SetGroups(groups []string) error {
|
||||
jsonData, err := json.Marshal(groups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.Groups = string(jsonData)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GetAllAuthProviders returns all authentication providers
|
||||
func (db *DB) GetAllAuthProviders(ctx context.Context) ([]AuthProvider, error) {
|
||||
var providers []AuthProvider
|
||||
tx := db.WithContext(ctx).Order("name asc").Find(&providers)
|
||||
if tx.Error != nil {
|
||||
return nil, fmt.Errorf("failed to get auth providers: %w", tx.Error)
|
||||
}
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// GetAuthProviderByID retrieves an authentication provider by ID
|
||||
func (db *DB) GetAuthProviderByID(ctx context.Context, id uint) (*AuthProvider, error) {
|
||||
var provider AuthProvider
|
||||
tx := db.WithContext(ctx).First(&provider, id)
|
||||
if tx.Error != nil {
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("auth provider not found: %d", id)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get auth provider: %w", tx.Error)
|
||||
}
|
||||
return &provider, nil
|
||||
}
|
||||
|
||||
// CreateAuthProvider creates a new authentication provider
|
||||
func (db *DB) CreateAuthProvider(ctx context.Context, provider *AuthProvider) error {
|
||||
tx := db.WithContext(ctx).Create(provider)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("failed to create auth provider: %w", tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAuthProvider updates an existing authentication provider
|
||||
func (db *DB) UpdateAuthProvider(ctx context.Context, provider *AuthProvider) error {
|
||||
tx := db.WithContext(ctx).Save(provider)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("failed to update auth provider: %w", tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAuthProvider deletes an authentication provider by ID
|
||||
func (db *DB) DeleteAuthProvider(ctx context.Context, id uint) error {
|
||||
tx := db.WithContext(ctx).Delete(&AuthProvider{}, id)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("failed to delete auth provider: %w", tx.Error)
|
||||
}
|
||||
if tx.RowsAffected == 0 {
|
||||
return fmt.Errorf("auth provider not found: %d", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEnabledAuthProviders returns all enabled authentication providers
|
||||
func (db *DB) GetEnabledAuthProviders(ctx context.Context) ([]AuthProvider, error) {
|
||||
var providers []AuthProvider
|
||||
tx := db.WithContext(ctx).Where("enabled = ?", true).Order("name asc").Find(&providers)
|
||||
if tx.Error != nil {
|
||||
return nil, fmt.Errorf("failed to get enabled auth providers: %w", tx.Error)
|
||||
}
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// GetAuthProviderByType returns authentication providers of a specific type
|
||||
func (db *DB) GetAuthProviderByType(ctx context.Context, providerType ProviderType) ([]AuthProvider, error) {
|
||||
var providers []AuthProvider
|
||||
tx := db.WithContext(ctx).Where("type = ?", providerType).Order("name asc").Find(&providers)
|
||||
if tx.Error != nil {
|
||||
return nil, fmt.Errorf("failed to get auth providers by type: %w", tx.Error)
|
||||
}
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// GetExternalUserIdentitiesByProviderID returns all external user identities for a specific provider
|
||||
func (db *DB) GetExternalUserIdentitiesByProviderID(ctx context.Context, providerID uint) ([]ExternalUserIdentity, error) {
|
||||
var identities []ExternalUserIdentity
|
||||
tx := db.WithContext(ctx).Where("provider_id = ?", providerID).Find(&identities)
|
||||
if tx.Error != nil {
|
||||
return nil, fmt.Errorf("failed to get external user identities: %w", tx.Error)
|
||||
}
|
||||
return identities, nil
|
||||
}
|
||||
|
||||
// CountExternalUserIdentitiesByProviderID counts the number of external user identities for a specific provider
|
||||
func (db *DB) CountExternalUserIdentitiesByProviderID(ctx context.Context, providerID uint) (int64, error) {
|
||||
var count int64
|
||||
tx := db.WithContext(ctx).Model(&ExternalUserIdentity{}).Where("provider_id = ?", providerID).Count(&count)
|
||||
if tx.Error != nil {
|
||||
return 0, fmt.Errorf("failed to count external user identities: %w", tx.Error)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// GetExternalUserIdentity gets an external user identity by provider ID and external ID
|
||||
func (db *DB) GetExternalUserIdentity(ctx context.Context, providerID uint, externalID string) (*ExternalUserIdentity, error) {
|
||||
var identity ExternalUserIdentity
|
||||
tx := db.WithContext(ctx).Where("provider_id = ? AND external_id = ?", providerID, externalID).First(&identity)
|
||||
if tx.Error != nil {
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil // Not found, but not an error
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get external user identity: %w", tx.Error)
|
||||
}
|
||||
return &identity, nil
|
||||
}
|
||||
|
||||
// CreateExternalUserIdentity creates a new external user identity
|
||||
func (db *DB) CreateExternalUserIdentity(ctx context.Context, identity *ExternalUserIdentity) error {
|
||||
tx := db.WithContext(ctx).Create(identity)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("failed to create external user identity: %w", tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateExternalUserIdentity updates an existing external user identity
|
||||
func (db *DB) UpdateExternalUserIdentity(ctx context.Context, identity *ExternalUserIdentity) error {
|
||||
tx := db.WithContext(ctx).Save(identity)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("failed to update external user identity: %w", tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteExternalUserIdentity deletes an external user identity by ID
|
||||
func (db *DB) DeleteExternalUserIdentity(ctx context.Context, id uint) error {
|
||||
tx := db.WithContext(ctx).Delete(&ExternalUserIdentity{}, id)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("failed to delete external user identity: %w", tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAuthProviderLastUsed updates the last used timestamp and increments the successful logins counter
|
||||
func (db *DB) UpdateAuthProviderLastUsed(ctx context.Context, providerID uint) error {
|
||||
tx := db.WithContext(ctx).Model(&AuthProvider{}).
|
||||
Where("id = ?", providerID).
|
||||
Updates(map[string]interface{}{
|
||||
"last_used": gorm.Expr("NOW()"),
|
||||
"successful_logins": gorm.Expr("successful_logins + 1"),
|
||||
})
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("failed to update auth provider last used: %w", tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -2079,3 +2080,30 @@ func (db *DB) GetRcloneCommandFlagsMap(commandID uint) (map[uint]RcloneCommandFl
|
||||
|
||||
return flagsMap, nil
|
||||
}
|
||||
|
||||
// GetEnabledAuthProviders returns all enabled authentication providers
|
||||
// func (db *DB) GetEnabledAuthProviders(ctx context.Context) ([]AuthProvider, error) {
|
||||
// var providers []AuthProvider
|
||||
// result := db.WithContext(ctx).Where("enabled = ?", true).Find(&providers)
|
||||
// return providers, result.Error
|
||||
// }
|
||||
|
||||
// GetExternalIdentity retrieves an external identity by provider ID and external ID
|
||||
func (db *DB) GetExternalIdentity(ctx context.Context, providerID uint, externalID string) (*ExternalUserIdentity, error) {
|
||||
var identity ExternalUserIdentity
|
||||
result := db.WithContext(ctx).Where("provider_id = ? AND external_id = ?", providerID, externalID).First(&identity)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
return &identity, nil
|
||||
}
|
||||
|
||||
// CreateExternalIdentity creates a new external user identity
|
||||
func (db *DB) CreateExternalIdentity(ctx context.Context, identity *ExternalUserIdentity) error {
|
||||
return db.WithContext(ctx).Create(identity).Error
|
||||
}
|
||||
|
||||
// UpdateExternalIdentity updates an existing external user identity
|
||||
func (db *DB) UpdateExternalIdentity(ctx context.Context, identity *ExternalUserIdentity) error {
|
||||
return db.WithContext(ctx).Save(identity).Error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AddAuthProviders adds tables for external authentication providers
|
||||
func AddAuthProviders() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "011_add_auth_providers",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
// Check if any tables exist (indicating an existing database)
|
||||
var count int64
|
||||
if err := tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Scan(&count).Error; err != nil {
|
||||
return fmt.Errorf("failed to check for existing tables: %v", err)
|
||||
}
|
||||
|
||||
// If tables exist, create a backup
|
||||
if count > 0 {
|
||||
// Get the database path
|
||||
sqlDB, err := tx.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get underlying database: %v", err)
|
||||
}
|
||||
|
||||
var seq int
|
||||
var name, dbPath string
|
||||
if err := sqlDB.QueryRow("PRAGMA database_list").Scan(&seq, &name, &dbPath); err != nil {
|
||||
return fmt.Errorf("failed to get database path: %v", err)
|
||||
}
|
||||
|
||||
// Get backup directory from environment variable or use default
|
||||
backupDir := os.Getenv("BACKUP_DIR")
|
||||
if backupDir == "" {
|
||||
backupDir = "/app/backups" // Default Docker path
|
||||
// Check if we're not in Docker
|
||||
if _, err := os.Stat(backupDir); os.IsNotExist(err) {
|
||||
backupDir = "backups" // Fallback to local directory
|
||||
}
|
||||
}
|
||||
|
||||
// Create backup directory if it doesn't exist
|
||||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create backup directory: %v", err)
|
||||
}
|
||||
|
||||
// Create backup file with timestamp in the backup directory
|
||||
dbFileName := filepath.Base(dbPath)
|
||||
backupFileName := fmt.Sprintf("%s.backup.%s", dbFileName, time.Now().Format("20060102_150405"))
|
||||
backupFile := filepath.Join(backupDir, backupFileName)
|
||||
|
||||
// Read original database
|
||||
data, err := os.ReadFile(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read database for backup: %v", err)
|
||||
}
|
||||
|
||||
// Write backup
|
||||
if err := os.WriteFile(backupFile, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write database backup: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Created database backup at %s\n", backupFile)
|
||||
}
|
||||
|
||||
// Create auth_providers table
|
||||
if err := tx.Exec(`CREATE TABLE IF NOT EXISTS auth_providers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
description TEXT,
|
||||
provider_url TEXT,
|
||||
icon_url TEXT,
|
||||
client_id VARCHAR(255),
|
||||
client_secret VARCHAR(255),
|
||||
redirect_url TEXT,
|
||||
scopes TEXT,
|
||||
attribute_mapping TEXT,
|
||||
config TEXT,
|
||||
successful_logins INTEGER DEFAULT 0,
|
||||
last_used DATETIME,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME
|
||||
)`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create external_user_identities table
|
||||
if err := tx.Exec(`CREATE TABLE IF NOT EXISTS external_user_identities (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider_id INTEGER NOT NULL,
|
||||
provider_type VARCHAR(50) NOT NULL,
|
||||
external_id VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
username VARCHAR(255),
|
||||
display_name VARCHAR(255),
|
||||
groups TEXT,
|
||||
last_login DATETIME,
|
||||
provider_data TEXT,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (provider_id) REFERENCES auth_providers(id) ON DELETE CASCADE
|
||||
)`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create unique index on provider_id and external_id
|
||||
if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_external_user_identities_provider_external
|
||||
ON external_user_identities(provider_id, external_id)`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
if err := tx.Exec("DROP TABLE IF EXISTS external_user_identities").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Exec("DROP TABLE IF EXISTS auth_providers").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ func GetMigrations(db *gorm.DB) *gormigrate.Gormigrate {
|
||||
AddUserNotifications(), // 008
|
||||
AddRcloneTables(), // 009
|
||||
AddRcloneCommandToConfig(), // 010
|
||||
AddAuthProviders(), // 011
|
||||
)
|
||||
|
||||
return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
|
||||
|
||||
@@ -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