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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user