build: Update library assets with UI visibility fix

- Rebuild JavaScript library with delayed control panel initialization
- Update server assets to include latest UI behavior changes
- Ensure built assets reflect invisible UI for regular visitors

The control panel now only appears after gate activation, maintaining
the invisible CMS principle for end users.
This commit is contained in:
2025-09-17 19:12:52 +02:00
parent 988f99f58b
commit 2a0915dda0
13 changed files with 694 additions and 82 deletions

View File

@@ -1,13 +1,19 @@
package auth
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/oauth2"
)
// UserInfo represents authenticated user information
@@ -21,8 +27,10 @@ type UserInfo struct {
// AuthConfig holds authentication configuration
type AuthConfig struct {
DevMode bool
Provider string
JWTSecret string
OAuthConfigs map[string]OAuthConfig
OIDC *OIDCConfig
}
// OAuthConfig holds OAuth provider configuration
@@ -33,14 +41,63 @@ type OAuthConfig struct {
Scopes []string
}
// OIDCConfig holds OIDC configuration for Authentik
type OIDCConfig struct {
Endpoint string
ClientID string
ClientSecret string
RedirectURL string
Scopes []string
}
// AuthService handles authentication operations
type AuthService struct {
config *AuthConfig
config *AuthConfig
provider *oidc.Provider
oauth2 *oauth2.Config
}
// NewAuthService creates a new authentication service
func NewAuthService(config *AuthConfig) *AuthService {
return &AuthService{config: config}
func NewAuthService(config *AuthConfig) (*AuthService, error) {
service := &AuthService{config: config}
// Initialize OIDC provider if configured
if config.Provider == "authentik" && config.OIDC != nil {
if err := service.initOIDC(); err != nil {
return nil, fmt.Errorf("failed to initialize OIDC: %w", err)
}
}
return service, nil
}
// initOIDC initializes the OIDC provider and OAuth2 config
func (a *AuthService) initOIDC() error {
ctx := context.Background()
// Create OIDC provider
provider, err := oidc.NewProvider(ctx, a.config.OIDC.Endpoint)
if err != nil {
return fmt.Errorf("failed to create OIDC provider: %w", err)
}
a.provider = provider
// Default scopes if none specified
scopes := a.config.OIDC.Scopes
if len(scopes) == 0 {
scopes = []string{oidc.ScopeOpenID, "profile", "email"}
}
// Create OAuth2 config
a.oauth2 = &oauth2.Config{
ClientID: a.config.OIDC.ClientID,
ClientSecret: a.config.OIDC.ClientSecret,
RedirectURL: a.config.OIDC.RedirectURL,
Endpoint: provider.Endpoint(),
Scopes: scopes,
}
return nil
}
// ExtractUserFromRequest extracts user information from HTTP request
@@ -74,7 +131,7 @@ func (a *AuthService) parseMockToken(token string) *UserInfo {
return &UserInfo{
ID: parts[1], // user part
Email: fmt.Sprintf("%s@localhost", parts[1]),
Name: strings.Title(parts[1]),
Name: strings.ToTitle(parts[1][:1]) + parts[1][1:],
Provider: "insertr-dev",
}
}
@@ -84,6 +141,60 @@ func (a *AuthService) parseMockToken(token string) *UserInfo {
// parseJWT parses and validates a real JWT token
func (a *AuthService) parseJWT(tokenString string) (*UserInfo, error) {
// Use OIDC verification if available
if a.config.Provider == "authentik" && a.provider != nil {
return a.parseOIDCToken(tokenString)
}
// Fallback to HMAC verification for other tokens
return a.parseHMACToken(tokenString)
}
// parseOIDCToken parses and validates an OIDC token using the provider's keys
func (a *AuthService) parseOIDCToken(tokenString string) (*UserInfo, error) {
ctx := context.Background()
// Create verifier
verifier := a.provider.Verifier(&oidc.Config{ClientID: a.config.OIDC.ClientID})
// Verify the token
idToken, err := verifier.Verify(ctx, tokenString)
if err != nil {
return nil, fmt.Errorf("failed to verify OIDC token: %w", err)
}
// Extract claims
var claims struct {
Sub string `json:"sub"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Name string `json:"name"`
PreferredName string `json:"preferred_username"`
Groups []string `json:"groups"`
}
if err := idToken.Claims(&claims); err != nil {
return nil, fmt.Errorf("failed to extract claims: %w", err)
}
// Build user info
userInfo := &UserInfo{
ID: claims.Sub,
Email: claims.Email,
Name: claims.Name,
Provider: idToken.Issuer,
}
// Use preferred_username if name is empty
if userInfo.Name == "" {
userInfo.Name = claims.PreferredName
}
return userInfo, nil
}
// parseHMACToken parses and validates a JWT token using HMAC signing
func (a *AuthService) parseHMACToken(tokenString string) (*UserInfo, error) {
// Parse the token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Validate signing method
@@ -167,6 +278,28 @@ func (a *AuthService) ValidateToken(tokenString string) error {
return err
}
// generateCodeVerifier generates a cryptographically secure code verifier for PKCE
func generateCodeVerifier() (string, error) {
data := make([]byte, 32)
if _, err := rand.Read(data); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(data), nil
}
// generateCodeChallenge generates a code challenge from the verifier using SHA256
func generateCodeChallenge(verifier string) string {
hash := sha256.Sum256([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(hash[:])
}
// generateState generates a random state parameter for OAuth2
func generateState() string {
data := make([]byte, 16)
rand.Read(data)
return base64.RawURLEncoding.EncodeToString(data)
}
// RefreshToken creates a new token with extended expiration
func (a *AuthService) RefreshToken(tokenString string) (string, error) {
userInfo, err := a.parseJWT(tokenString)
@@ -216,16 +349,10 @@ func (a *AuthService) RequireAuth(next http.Handler) http.Handler {
// HandleOAuthLogin initiates OAuth flow
func (a *AuthService) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
provider := r.URL.Query().Get("provider")
if provider == "" {
provider = "google"
}
// TODO: Implement OAuth initiation
// For now, return mock success in dev mode
if a.config.DevMode {
// Handle mock authentication in dev mode
if a.config.DevMode && a.config.Provider == "mock" {
response := map[string]interface{}{
"message": "OAuth login not yet implemented",
"message": "Mock OAuth login",
"redirect_url": "/auth/callback?code=mock_code&state=mock_state",
"dev_mode": true,
}
@@ -234,17 +361,64 @@ func (a *AuthService) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
return
}
http.Error(w, "OAuth not implemented", http.StatusNotImplemented)
// Handle Authentik OIDC flow
if a.config.Provider == "authentik" && a.oauth2 != nil {
// Generate PKCE parameters
codeVerifier, err := generateCodeVerifier()
if err != nil {
http.Error(w, "Failed to generate code verifier", http.StatusInternalServerError)
return
}
codeChallenge := generateCodeChallenge(codeVerifier)
state := generateState()
// Store PKCE parameters in session/cookie (simplified for now)
// In production, you'd store this in a secure session store
http.SetCookie(w, &http.Cookie{
Name: "code_verifier",
Value: codeVerifier,
Path: "/",
HttpOnly: true,
Secure: !a.config.DevMode,
SameSite: http.SameSiteStrictMode,
MaxAge: 600, // 10 minutes
})
http.SetCookie(w, &http.Cookie{
Name: "oauth_state",
Value: state,
Path: "/",
HttpOnly: true,
Secure: !a.config.DevMode,
SameSite: http.SameSiteStrictMode,
MaxAge: 600, // 10 minutes
})
// Build authorization URL with PKCE
authURL := a.oauth2.AuthCodeURL(state,
oauth2.SetAuthURLParam("code_challenge", codeChallenge),
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
)
response := map[string]interface{}{
"redirect_url": authURL,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
return
}
http.Error(w, "Authentication provider not configured", http.StatusBadRequest)
}
// HandleOAuthCallback handles OAuth provider callback
func (a *AuthService) HandleOAuthCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
_ = r.URL.Query().Get("state") // TODO: validate state parameter
state := r.URL.Query().Get("state")
// TODO: Implement OAuth token exchange
// For now, return mock token in dev mode
if a.config.DevMode && code != "" {
// Handle mock authentication in dev mode
if a.config.DevMode && a.config.Provider == "mock" && code != "" {
mockToken, err := a.CreateMockJWT("dev-user", "dev@localhost", "Development User")
if err != nil {
http.Error(w, "Failed to create mock token", http.StatusInternalServerError)
@@ -261,5 +435,50 @@ func (a *AuthService) HandleOAuthCallback(w http.ResponseWriter, r *http.Request
return
}
http.Error(w, "OAuth callback not implemented", http.StatusNotImplemented)
// Handle Authentik OIDC callback
if a.config.Provider == "authentik" && a.oauth2 != nil {
// Validate state parameter
stateCookie, err := r.Cookie("oauth_state")
if err != nil || stateCookie.Value != state {
http.Error(w, "Invalid state parameter", http.StatusBadRequest)
return
}
// Get code verifier from cookie
verifierCookie, err := r.Cookie("code_verifier")
if err != nil {
http.Error(w, "Missing code verifier", http.StatusBadRequest)
return
}
// Exchange code for token with PKCE
ctx := context.Background()
token, err := a.oauth2.Exchange(ctx, code,
oauth2.SetAuthURLParam("code_verifier", verifierCookie.Value),
)
if err != nil {
http.Error(w, fmt.Sprintf("Token exchange failed: %v", err), http.StatusBadRequest)
return
}
// Extract ID token
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
http.Error(w, "No ID token in response", http.StatusBadRequest)
return
}
// Clear cookies
http.SetCookie(w, &http.Cookie{Name: "oauth_state", MaxAge: -1, Path: "/"})
http.SetCookie(w, &http.Cookie{Name: "code_verifier", MaxAge: -1, Path: "/"})
response := map[string]interface{}{
"token": rawIDToken,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
return
}
http.Error(w, "Authentication provider not configured", http.StatusBadRequest)
}