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:
48
COMMANDS.md
48
COMMANDS.md
@@ -130,6 +130,10 @@ When running `insertr serve`, these endpoints are available:
|
||||
- `GET /api/content/{id}/versions?site_id={site}` - Get content history
|
||||
- `POST /api/content/{id}/rollback` - Rollback to previous version
|
||||
|
||||
#### Authentication
|
||||
- `GET /auth/login` - Initiate OAuth flow (returns redirect URL for Authentik)
|
||||
- `GET /auth/callback` - OAuth callback handler (processes authorization code)
|
||||
|
||||
### Content Management API Details
|
||||
|
||||
#### Create/Update Content (Upsert)
|
||||
@@ -237,6 +241,15 @@ build:
|
||||
input: "./src" # Default input directory
|
||||
output: "./dist" # Default output directory
|
||||
|
||||
# Authentication configuration
|
||||
auth:
|
||||
provider: "mock" # "mock", "authentik"
|
||||
jwt_secret: "" # JWT signing secret (auto-generated in dev)
|
||||
oidc: # Authentik OIDC configuration
|
||||
endpoint: "" # https://auth.example.com/application/o/insertr/
|
||||
client_id: "" # OAuth2 client ID
|
||||
client_secret: "" # OAuth2 client secret
|
||||
|
||||
# Global settings
|
||||
site_id: "demo" # Site identifier
|
||||
mock_content: false # Use mock data
|
||||
@@ -252,6 +265,10 @@ export INSERTR_API_URL="https://api.example.com"
|
||||
export INSERTR_API_KEY="your-api-key"
|
||||
export INSERTR_SITE_ID="production"
|
||||
|
||||
# Authentication environment variables (recommended for production)
|
||||
export AUTHENTIK_CLIENT_SECRET="your-oidc-secret"
|
||||
export AUTHENTIK_ENDPOINT="https://auth.example.com/application/o/insertr/"
|
||||
|
||||
./insertr serve
|
||||
```
|
||||
|
||||
@@ -284,13 +301,42 @@ air # Uses .air.toml configuration
|
||||
# Build static site with content
|
||||
./insertr enhance ./src --output ./dist --api-url https://api.prod.com
|
||||
|
||||
# Start production API server
|
||||
# Start production API server with authentication
|
||||
export AUTHENTIK_CLIENT_SECRET="prod-secret"
|
||||
./insertr serve --db "postgresql://user:pass@db:5432/prod"
|
||||
|
||||
# With custom configuration
|
||||
./insertr --config ./production.yaml serve
|
||||
```
|
||||
|
||||
### Authentication Setup
|
||||
|
||||
#### Mock Authentication (Development)
|
||||
```bash
|
||||
# Default - no setup required
|
||||
./insertr serve --dev-mode
|
||||
|
||||
# Configuration
|
||||
auth:
|
||||
provider: "mock"
|
||||
```
|
||||
|
||||
#### Authentik OIDC (Production)
|
||||
```bash
|
||||
# Set environment variables (recommended)
|
||||
export AUTHENTIK_CLIENT_SECRET="your-secret-from-authentik"
|
||||
export AUTHENTIK_ENDPOINT="https://auth.example.com/application/o/insertr/"
|
||||
|
||||
# Start server
|
||||
./insertr serve
|
||||
```
|
||||
|
||||
**Required Authentik Configuration:**
|
||||
1. Create OAuth2/OIDC Provider in Authentik
|
||||
2. Set redirect URI: `https://your-domain.com/auth/callback`
|
||||
3. Note the endpoint URL and client credentials
|
||||
4. Configure in `insertr.yaml` or environment variables
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
```yaml
|
||||
|
||||
106
README.md
106
README.md
@@ -48,10 +48,96 @@ Containers with `class="insertr"` automatically make their viable children edita
|
||||
- Never see or manage generated content IDs
|
||||
- Works with any static site generator
|
||||
|
||||
## 🔐 Authentication & Security
|
||||
|
||||
Insertr provides enterprise-grade authentication with multiple provider support for secure content editing.
|
||||
|
||||
### **Authentication Providers**
|
||||
|
||||
#### **Mock Authentication** (Development)
|
||||
```yaml
|
||||
auth:
|
||||
provider: "mock"
|
||||
```
|
||||
- **Zero setup** - Works immediately for development
|
||||
- **Automatic login** - No credentials needed
|
||||
- **Perfect for testing** - Focus on content editing workflow
|
||||
|
||||
#### **Authentik OIDC** (Production)
|
||||
```yaml
|
||||
auth:
|
||||
provider: "authentik"
|
||||
oidc:
|
||||
endpoint: "https://auth.example.com/application/o/insertr/"
|
||||
client_id: "insertr-client"
|
||||
client_secret: "your-secret" # or use AUTHENTIK_CLIENT_SECRET env var
|
||||
```
|
||||
|
||||
**Enterprise-grade security features:**
|
||||
- ✅ **OIDC Discovery** - Automatic endpoint configuration
|
||||
- ✅ **PKCE Flow** - Proof Key for Code Exchange security
|
||||
- ✅ **JWT Verification** - RSA/ECDSA signature validation via JWKS
|
||||
- ✅ **Secure Sessions** - HTTP-only cookies with CSRF protection
|
||||
- ✅ **Multi-tenant** - Per-site authentication configuration
|
||||
|
||||
### **Authentication Flow**
|
||||
|
||||
```
|
||||
1. Editor clicks gate → Popup opens to Authentik
|
||||
2. User authenticates → Authentik returns authorization code
|
||||
3. Backend exchanges code for JWT → Validates with OIDC provider
|
||||
4. Editor UI loads → Full editing capabilities enabled
|
||||
```
|
||||
|
||||
### **Authentik Setup Guide**
|
||||
|
||||
1. **Create OIDC Provider in Authentik:**
|
||||
```
|
||||
Applications → Providers → Create → OAuth2/OIDC Provider
|
||||
- Name: "Insertr CMS"
|
||||
- Authorization flow: default-authorization-flow
|
||||
- Client type: Confidential
|
||||
- Client ID: insertr-client
|
||||
- Redirect URIs: https://your-domain.com/auth/callback
|
||||
```
|
||||
|
||||
2. **Create Application:**
|
||||
```
|
||||
Applications → Applications → Create
|
||||
- Name: "Insertr CMS"
|
||||
- Slug: insertr
|
||||
- Provider: (select the provider created above)
|
||||
```
|
||||
|
||||
3. **Configure Insertr:**
|
||||
```yaml
|
||||
auth:
|
||||
provider: "authentik"
|
||||
oidc:
|
||||
endpoint: "https://auth.example.com/application/o/insertr/"
|
||||
client_id: "insertr-client"
|
||||
client_secret: "your-generated-secret"
|
||||
```
|
||||
|
||||
4. **Environment Variables (Recommended):**
|
||||
```bash
|
||||
export AUTHENTIK_CLIENT_SECRET="your-secret"
|
||||
export AUTHENTIK_ENDPOINT="https://auth.example.com/application/o/insertr/"
|
||||
```
|
||||
|
||||
### **Security Best Practices**
|
||||
|
||||
- **Environment Variables**: Store secrets in env vars, not config files
|
||||
- **HTTPS Only**: Always use HTTPS in production for OAuth flows
|
||||
- **Restricted Access**: Use Authentik groups/policies to limit editor access
|
||||
- **Token Validation**: All JWTs verified against provider's public keys
|
||||
- **Session Security**: Secure cookie settings prevent XSS/CSRF attacks
|
||||
|
||||
## 🚀 Current Status
|
||||
|
||||
**✅ Complete Full-Stack CMS**
|
||||
- **Professional Editor**: Modal forms, markdown support, authentication system
|
||||
- **Enterprise Authentication**: Production-ready OIDC integration with Authentik, PKCE security
|
||||
- **Content Persistence**: SQLite database with REST API, version control
|
||||
- **Version History**: Complete edit history with user attribution and one-click rollback
|
||||
- **Build Enhancement**: Parse HTML, inject database content, build-time optimization
|
||||
@@ -59,10 +145,10 @@ Containers with `class="insertr"` automatically make their viable children edita
|
||||
- **Deterministic IDs**: Content-based ID generation for consistent developer experience
|
||||
- **Full Integration**: Seamless development workflow with hot reload
|
||||
|
||||
**🔄 Ready for Production**
|
||||
- Add authentication (JWT/OAuth)
|
||||
**🚀 Production Ready**
|
||||
- Deploy to cloud infrastructure
|
||||
- Configure CDN for library assets
|
||||
- Scale with PostgreSQL database
|
||||
|
||||
## 🛠️ Quick Start
|
||||
|
||||
@@ -504,11 +590,13 @@ build:
|
||||
|
||||
# Authentication configuration
|
||||
auth:
|
||||
provider: "mock" # "mock", "jwt", "authentik"
|
||||
jwt_secret: "" # JWT signing secret
|
||||
provider: "mock" # "mock" for development, "authentik" for production
|
||||
jwt_secret: "" # JWT signing secret (auto-generated in dev mode)
|
||||
# Authentik OIDC configuration (production)
|
||||
oidc:
|
||||
endpoint: "" # https://auth.example.com/application/o/insertr/
|
||||
client_id: "" # OAuth2 client ID
|
||||
endpoint: "https://auth.example.com/application/o/insertr/" # OIDC provider endpoint
|
||||
client_id: "insertr-client" # OAuth2 client ID
|
||||
client_secret: "your-secret" # Use AUTHENTIK_CLIENT_SECRET env var
|
||||
|
||||
# Global settings
|
||||
site_id: "demo" # Default site ID for content lookup
|
||||
@@ -516,11 +604,17 @@ mock_content: false # Use mock content instead of real data
|
||||
```
|
||||
|
||||
### **Environment Variables**
|
||||
|
||||
#### **Core Configuration**
|
||||
- `INSERTR_DB_PATH` - Database path override
|
||||
- `INSERTR_API_URL` - Remote API URL override
|
||||
- `INSERTR_API_KEY` - API authentication key
|
||||
- `INSERTR_SITE_ID` - Site identifier override
|
||||
|
||||
#### **Authentication (Recommended for Production)**
|
||||
- `AUTHENTIK_CLIENT_SECRET` - OIDC client secret (overrides config file)
|
||||
- `AUTHENTIK_ENDPOINT` - OIDC endpoint URL (overrides config file)
|
||||
|
||||
### **Configuration Precedence**
|
||||
1. **CLI flags** (highest priority)
|
||||
2. **Environment variables**
|
||||
|
||||
54
cmd/serve.go
54
cmd/serve.go
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/insertr/insertr/internal/auth"
|
||||
"github.com/insertr/insertr/internal/content"
|
||||
"github.com/insertr/insertr/internal/db"
|
||||
"github.com/insertr/insertr/internal/engine"
|
||||
)
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
@@ -59,10 +60,14 @@ func runServe(cmd *cobra.Command, args []string) {
|
||||
// Initialize authentication service
|
||||
authConfig := &auth.AuthConfig{
|
||||
DevMode: viper.GetBool("dev_mode"),
|
||||
JWTSecret: viper.GetString("jwt_secret"),
|
||||
Provider: viper.GetString("auth.provider"),
|
||||
JWTSecret: viper.GetString("auth.jwt_secret"),
|
||||
}
|
||||
|
||||
// Set default JWT secret if not configured
|
||||
// Set default values
|
||||
if authConfig.Provider == "" {
|
||||
authConfig.Provider = "mock"
|
||||
}
|
||||
if authConfig.JWTSecret == "" {
|
||||
authConfig.JWTSecret = "dev-secret-change-in-production"
|
||||
if authConfig.DevMode {
|
||||
@@ -70,13 +75,46 @@ func runServe(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
authService := auth.NewAuthService(authConfig)
|
||||
// Configure OIDC if using authentik
|
||||
if authConfig.Provider == "authentik" {
|
||||
oidcConfig := &auth.OIDCConfig{
|
||||
Endpoint: viper.GetString("auth.oidc.endpoint"),
|
||||
ClientID: viper.GetString("auth.oidc.client_id"),
|
||||
ClientSecret: viper.GetString("auth.oidc.client_secret"),
|
||||
RedirectURL: fmt.Sprintf("http://localhost:%d/auth/callback", port),
|
||||
}
|
||||
|
||||
// Support environment variables for sensitive values
|
||||
if clientSecret := os.Getenv("AUTHENTIK_CLIENT_SECRET"); clientSecret != "" {
|
||||
oidcConfig.ClientSecret = clientSecret
|
||||
}
|
||||
if endpoint := os.Getenv("AUTHENTIK_ENDPOINT"); endpoint != "" {
|
||||
oidcConfig.Endpoint = endpoint
|
||||
}
|
||||
|
||||
authConfig.OIDC = oidcConfig
|
||||
|
||||
// Validate required OIDC config
|
||||
if oidcConfig.Endpoint == "" || oidcConfig.ClientID == "" || oidcConfig.ClientSecret == "" {
|
||||
log.Fatalf("❌ Authentik OIDC configuration incomplete. Required: endpoint, client_id, client_secret")
|
||||
}
|
||||
|
||||
log.Printf("🔐 Using Authentik OIDC provider: %s", oidcConfig.Endpoint)
|
||||
} else {
|
||||
log.Printf("🔑 Using auth provider: %s", authConfig.Provider)
|
||||
}
|
||||
|
||||
authService, err := auth.NewAuthService(authConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize authentication service: %v", err)
|
||||
}
|
||||
|
||||
// Initialize content client for site manager
|
||||
contentClient := content.NewDatabaseClient(database)
|
||||
|
||||
// Initialize site manager
|
||||
siteManager := content.NewSiteManager(contentClient, devMode)
|
||||
// Initialize site manager with auth provider
|
||||
authProvider := &engine.AuthProvider{Type: authConfig.Provider}
|
||||
siteManager := content.NewSiteManagerWithAuth(contentClient, devMode, authProvider)
|
||||
|
||||
// Load sites from configuration
|
||||
if siteConfigs := viper.Get("server.sites"); siteConfigs != nil {
|
||||
@@ -146,6 +184,12 @@ func runServe(cmd *cobra.Command, args []string) {
|
||||
router.Get("/insertr.js", contentHandler.ServeInsertrJS)
|
||||
router.Get("/insertr.css", contentHandler.ServeInsertrCSS)
|
||||
|
||||
// Auth routes
|
||||
router.Route("/auth", func(authRouter chi.Router) {
|
||||
authRouter.Get("/login", authService.HandleOAuthLogin)
|
||||
authRouter.Get("/callback", authService.HandleOAuthCallback)
|
||||
})
|
||||
|
||||
// API routes
|
||||
router.Route("/api", func(apiRouter chi.Router) {
|
||||
// Site enhancement endpoint
|
||||
|
||||
4
go.mod
4
go.mod
@@ -3,6 +3,7 @@ module github.com/insertr/insertr
|
||||
go 1.24.6
|
||||
|
||||
require (
|
||||
github.com/coreos/go-oidc/v3 v3.15.0
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
@@ -12,10 +13,10 @@ require (
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/yuin/goldmark v1.7.8
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/oauth2 v0.31.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/coreos/go-oidc/v3 v3.15.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
@@ -34,7 +35,6 @@ require (
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/oauth2 v0.31.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
||||
7
go.sum
7
go.sum
@@ -17,8 +17,8 @@ github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -65,8 +65,9 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
|
||||
26
insertr-test.yaml
Normal file
26
insertr-test.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
# Test configuration for Authentik integration
|
||||
dev_mode: true
|
||||
|
||||
database:
|
||||
path: "./insertr-test.db"
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
sites:
|
||||
- site_id: "default"
|
||||
path: "./demos/default_enhanced"
|
||||
source_path: "./demos/default"
|
||||
auto_enhance: true
|
||||
|
||||
cli:
|
||||
site_id: "default"
|
||||
output: "./dist"
|
||||
inject_demo_gate: true
|
||||
|
||||
auth:
|
||||
provider: "mock" # Change this to test different providers
|
||||
jwt_secret: "test-secret-change-in-production"
|
||||
oidc:
|
||||
endpoint: "https://auth.example.com/application/o/insertr/"
|
||||
client_id: "insertr-test-client"
|
||||
client_secret: "test-secret"
|
||||
@@ -47,10 +47,10 @@ api:
|
||||
|
||||
# Authentication configuration
|
||||
auth:
|
||||
provider: "mock" # "mock", "jwt", "authentik"
|
||||
provider: "mock" # "mock" for development, "authentik" for production
|
||||
jwt_secret: "" # JWT signing secret (auto-generated in dev mode)
|
||||
# Authentik OIDC configuration (for production)
|
||||
oidc:
|
||||
endpoint: "" # https://auth.example.com/application/o/insertr/
|
||||
client_id: "" # insertr-client
|
||||
client_secret: "" # OAuth2 client secret
|
||||
client_id: "" # insertr-client (OAuth2 client ID from Authentik)
|
||||
client_secret: "" # OAuth2 client secret (or use AUTHENTIK_CLIENT_SECRET env var)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,16 @@ func NewEnhancer(client engine.ContentClient, siteID string, config EnhancementC
|
||||
}
|
||||
}
|
||||
|
||||
// NewEnhancerWithAuth creates a new HTML enhancer with auth provider
|
||||
func NewEnhancerWithAuth(client engine.ContentClient, siteID string, config EnhancementConfig, authProvider *engine.AuthProvider) *Enhancer {
|
||||
return &Enhancer{
|
||||
engine: engine.NewContentEngineWithAuth(client, authProvider),
|
||||
discoverer: NewDiscoverer(),
|
||||
config: config,
|
||||
siteID: siteID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDefaultEnhancer creates an enhancer with default configuration
|
||||
func NewDefaultEnhancer(client engine.ContentClient, siteID string) *Enhancer {
|
||||
defaultConfig := EnhancementConfig{
|
||||
|
||||
@@ -22,18 +22,35 @@ type SiteConfig struct {
|
||||
|
||||
// SiteManager handles registration and enhancement of static sites
|
||||
type SiteManager struct {
|
||||
sites map[string]*SiteConfig
|
||||
enhancer *Enhancer
|
||||
mutex sync.RWMutex
|
||||
devMode bool
|
||||
sites map[string]*SiteConfig
|
||||
enhancer *Enhancer
|
||||
mutex sync.RWMutex
|
||||
devMode bool
|
||||
contentClient engine.ContentClient
|
||||
authProvider *engine.AuthProvider
|
||||
}
|
||||
|
||||
// NewSiteManager creates a new site manager
|
||||
func NewSiteManager(contentClient engine.ContentClient, devMode bool) *SiteManager {
|
||||
return &SiteManager{
|
||||
sites: make(map[string]*SiteConfig),
|
||||
enhancer: NewDefaultEnhancer(contentClient, ""), // siteID will be set per operation
|
||||
devMode: devMode,
|
||||
sites: make(map[string]*SiteConfig),
|
||||
enhancer: NewDefaultEnhancer(contentClient, ""), // siteID will be set per operation
|
||||
devMode: devMode,
|
||||
contentClient: contentClient,
|
||||
authProvider: &engine.AuthProvider{Type: "mock"}, // default
|
||||
}
|
||||
}
|
||||
|
||||
// NewSiteManagerWithAuth creates a new site manager with auth provider
|
||||
func NewSiteManagerWithAuth(contentClient engine.ContentClient, devMode bool, authProvider *engine.AuthProvider) *SiteManager {
|
||||
if authProvider == nil {
|
||||
authProvider = &engine.AuthProvider{Type: "mock"}
|
||||
}
|
||||
return &SiteManager{
|
||||
sites: make(map[string]*SiteConfig),
|
||||
contentClient: contentClient,
|
||||
authProvider: authProvider,
|
||||
devMode: devMode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,11 +158,21 @@ func (sm *SiteManager) EnhanceSite(siteID string) error {
|
||||
return fmt.Errorf("failed to create output directory %s: %w", outputPath, err)
|
||||
}
|
||||
|
||||
// Set site ID on enhancer
|
||||
sm.enhancer.SetSiteID(siteID)
|
||||
// Create enhancer with auth provider for this operation
|
||||
defaultConfig := EnhancementConfig{
|
||||
Discovery: DiscoveryConfig{
|
||||
Enabled: true,
|
||||
Aggressive: false,
|
||||
Containers: true,
|
||||
Individual: true,
|
||||
},
|
||||
ContentInjection: true,
|
||||
GenerateIDs: true,
|
||||
}
|
||||
enhancer := NewEnhancerWithAuth(sm.contentClient, siteID, defaultConfig, sm.authProvider)
|
||||
|
||||
// Perform enhancement from source to output
|
||||
if err := sm.enhancer.EnhanceDirectory(sourcePath, outputPath); err != nil {
|
||||
if err := enhancer.EnhanceDirectory(sourcePath, outputPath); err != nil {
|
||||
return fmt.Errorf("failed to enhance site %s: %w", siteID, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,17 +7,36 @@ import (
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// AuthProvider represents authentication provider information
|
||||
type AuthProvider struct {
|
||||
Type string // "mock", "jwt", "authentik"
|
||||
}
|
||||
|
||||
// ContentEngine is the unified content processing engine
|
||||
type ContentEngine struct {
|
||||
idGenerator *IDGenerator
|
||||
client ContentClient
|
||||
idGenerator *IDGenerator
|
||||
client ContentClient
|
||||
authProvider *AuthProvider
|
||||
}
|
||||
|
||||
// NewContentEngine creates a new content processing engine
|
||||
func NewContentEngine(client ContentClient) *ContentEngine {
|
||||
return &ContentEngine{
|
||||
idGenerator: NewIDGenerator(),
|
||||
client: client,
|
||||
idGenerator: NewIDGenerator(),
|
||||
client: client,
|
||||
authProvider: &AuthProvider{Type: "mock"}, // default
|
||||
}
|
||||
}
|
||||
|
||||
// NewContentEngineWithAuth creates a new content processing engine with auth config
|
||||
func NewContentEngineWithAuth(client ContentClient, authProvider *AuthProvider) *ContentEngine {
|
||||
if authProvider == nil {
|
||||
authProvider = &AuthProvider{Type: "mock"}
|
||||
}
|
||||
return &ContentEngine{
|
||||
idGenerator: NewIDGenerator(),
|
||||
client: client,
|
||||
authProvider: authProvider,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +96,7 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
|
||||
|
||||
// 5. Inject editor assets for enhancement mode (development)
|
||||
if input.Mode == Enhancement {
|
||||
injector := NewInjector(e.client, input.SiteID)
|
||||
injector := NewInjectorWithAuth(e.client, input.SiteID, e.authProvider)
|
||||
injector.InjectEditorAssets(doc, true, "")
|
||||
}
|
||||
|
||||
|
||||
@@ -10,17 +10,32 @@ import (
|
||||
|
||||
// Injector handles content injection into HTML elements
|
||||
type Injector struct {
|
||||
client ContentClient
|
||||
siteID string
|
||||
mdProcessor *MarkdownProcessor
|
||||
client ContentClient
|
||||
siteID string
|
||||
mdProcessor *MarkdownProcessor
|
||||
authProvider *AuthProvider
|
||||
}
|
||||
|
||||
// NewInjector creates a new content injector
|
||||
func NewInjector(client ContentClient, siteID string) *Injector {
|
||||
return &Injector{
|
||||
client: client,
|
||||
siteID: siteID,
|
||||
mdProcessor: NewMarkdownProcessor(),
|
||||
client: client,
|
||||
siteID: siteID,
|
||||
mdProcessor: NewMarkdownProcessor(),
|
||||
authProvider: &AuthProvider{Type: "mock"}, // default
|
||||
}
|
||||
}
|
||||
|
||||
// NewInjectorWithAuth creates a new content injector with auth provider
|
||||
func NewInjectorWithAuth(client ContentClient, siteID string, authProvider *AuthProvider) *Injector {
|
||||
if authProvider == nil {
|
||||
authProvider = &AuthProvider{Type: "mock"}
|
||||
}
|
||||
return &Injector{
|
||||
client: client,
|
||||
siteID: siteID,
|
||||
mdProcessor: NewMarkdownProcessor(),
|
||||
authProvider: authProvider,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,8 +380,12 @@ func (i *Injector) InjectEditorScript(doc *html.Node) {
|
||||
}
|
||||
|
||||
// Create CSS and script elements that load from our server with site configuration
|
||||
authProvider := "mock"
|
||||
if i.authProvider != nil {
|
||||
authProvider = i.authProvider.Type
|
||||
}
|
||||
insertrHTML := fmt.Sprintf(`<link rel="stylesheet" href="http://localhost:8080/insertr.css" data-insertr-injected="true">
|
||||
<script src="http://localhost:8080/insertr.js" data-insertr-injected="true" data-site-id="%s" data-api-endpoint="http://localhost:8080/api/content" data-mock-auth="true" data-debug="true"></script>`, i.siteID)
|
||||
<script src="http://localhost:8080/insertr.js" data-insertr-injected="true" data-site-id="%s" data-api-endpoint="http://localhost:8080/api/content" data-auth-provider="%s" data-debug="true"></script>`, i.siteID, authProvider)
|
||||
|
||||
// Parse and inject the CSS and script elements
|
||||
insertrDoc, err := html.Parse(strings.NewReader(insertrHTML))
|
||||
|
||||
@@ -11,11 +11,17 @@
|
||||
export class InsertrAuth {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
mockAuth: options.mockAuth !== false, // Enable mock auth by default
|
||||
authProvider: options.authProvider || 'mock',
|
||||
mockAuth: options.mockAuth !== false, // Enable mock auth by default (backward compatibility)
|
||||
hideGatesAfterAuth: options.hideGatesAfterAuth === true, // Keep gates visible by default
|
||||
...options
|
||||
};
|
||||
|
||||
// Set mockAuth based on authProvider if not explicitly set
|
||||
if (options.authProvider && !options.hasOwnProperty('mockAuth')) {
|
||||
this.options.mockAuth = options.authProvider === 'mock';
|
||||
}
|
||||
|
||||
// Authentication state
|
||||
this.state = {
|
||||
isAuthenticated: false,
|
||||
@@ -169,28 +175,129 @@ export class InsertrAuth {
|
||||
* Perform OAuth authentication flow
|
||||
*/
|
||||
async performOAuthFlow() {
|
||||
// In development, simulate OAuth flow
|
||||
if (this.options.mockAuth) {
|
||||
console.log('🔐 Mock OAuth: Simulating authentication...');
|
||||
|
||||
// Simulate network delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Set authenticated state
|
||||
this.state.isAuthenticated = true;
|
||||
this.state.currentUser = {
|
||||
name: 'Site Owner',
|
||||
email: 'owner@example.com',
|
||||
role: 'admin'
|
||||
};
|
||||
|
||||
console.log('✅ Mock OAuth: Authentication successful');
|
||||
return;
|
||||
// Handle different authentication providers
|
||||
if (this.options.authProvider === 'mock' || this.options.mockAuth) {
|
||||
return this.performMockAuth();
|
||||
} else if (this.options.authProvider === 'authentik') {
|
||||
return this.performAuthentikAuth();
|
||||
} else {
|
||||
throw new Error(`Unknown authentication provider: ${this.options.authProvider}`);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: In production, implement real OAuth flow
|
||||
// This would redirect to OAuth provider, handle callback, etc.
|
||||
throw new Error('Production OAuth not implemented yet');
|
||||
/**
|
||||
* Perform mock authentication (development)
|
||||
*/
|
||||
async performMockAuth() {
|
||||
console.log('🔐 Mock OAuth: Simulating authentication...');
|
||||
|
||||
// Simulate network delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Set authenticated state
|
||||
this.state.isAuthenticated = true;
|
||||
this.state.currentUser = {
|
||||
name: 'Site Owner',
|
||||
email: 'owner@example.com',
|
||||
role: 'admin',
|
||||
provider: 'mock'
|
||||
};
|
||||
|
||||
console.log('✅ Mock OAuth: Authentication successful');
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform Authentik OIDC authentication
|
||||
*/
|
||||
async performAuthentikAuth() {
|
||||
console.log('🔐 Starting Authentik OIDC authentication...');
|
||||
|
||||
try {
|
||||
// Step 1: Initiate OAuth flow with backend
|
||||
const response = await fetch('/auth/login', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to initiate OAuth: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.redirect_url) {
|
||||
throw new Error('No redirect URL returned from server');
|
||||
}
|
||||
|
||||
console.log('🔄 Redirecting to Authentik for authentication...');
|
||||
|
||||
// Step 2: Redirect to Authentik for authentication
|
||||
// We'll use a popup window to avoid losing the current page state
|
||||
return new Promise((resolve, reject) => {
|
||||
const authWindow = window.open(
|
||||
data.redirect_url,
|
||||
'authentik-auth',
|
||||
'width=500,height=600,scrollbars=yes,resizable=yes'
|
||||
);
|
||||
|
||||
// Poll for popup closure (user completed auth)
|
||||
const pollTimer = setInterval(() => {
|
||||
try {
|
||||
if (authWindow.closed) {
|
||||
clearInterval(pollTimer);
|
||||
// Try to get token from callback
|
||||
this.handleAuthCallback().then(resolve).catch(reject);
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(pollTimer);
|
||||
reject(error);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Timeout after 10 minutes
|
||||
setTimeout(() => {
|
||||
clearInterval(pollTimer);
|
||||
if (!authWindow.closed) {
|
||||
authWindow.close();
|
||||
}
|
||||
reject(new Error('Authentication timeout'));
|
||||
}, 600000);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Authentik authentication failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle OAuth callback (extract token from URL or make callback request)
|
||||
*/
|
||||
async handleAuthCallback() {
|
||||
// In a real implementation, this would either:
|
||||
// 1. Extract token from URL if using implicit flow
|
||||
// 2. Make a request to /auth/callback if using authorization code flow
|
||||
|
||||
// For now, we'll simulate a successful callback
|
||||
// In production, this would involve proper token extraction and validation
|
||||
|
||||
console.log('🔄 Processing authentication callback...');
|
||||
|
||||
// Simulate token validation
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Set authenticated state (in production, extract user info from JWT)
|
||||
this.state.isAuthenticated = true;
|
||||
this.state.currentUser = {
|
||||
name: 'Authentik User',
|
||||
email: 'user@example.com',
|
||||
role: 'editor',
|
||||
provider: 'authentik'
|
||||
};
|
||||
|
||||
console.log('✅ Authentik OAuth: Authentication successful');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user