mirror of
https://github.com/wahyd4/passkey-auth.git
synced 2026-08-09 04:15:55 +10:00
Revert "Merge pull request #9 from wahyd4/traefik-support"
This reverts commit01a625d8fd, reversing changes made to1aba3b3947.
This commit is contained in:
@@ -128,7 +128,7 @@ jobs:
|
|||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v3
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: --timeout=5m --tests=false
|
args: --timeout=5m
|
||||||
|
|
||||||
security:
|
security:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -59,4 +59,3 @@ dev-*
|
|||||||
web/test.html
|
web/test.html
|
||||||
|
|
||||||
*.log
|
*.log
|
||||||
test*.yaml
|
|
||||||
|
|||||||
@@ -37,6 +37,18 @@ docker-run: docker-build ## Run with Docker Compose
|
|||||||
@docker-compose up -d
|
@docker-compose up -d
|
||||||
@echo "Access the application at http://localhost:8080"
|
@echo "Access the application at http://localhost:8080"
|
||||||
|
|
||||||
|
docker-stop: ## Stop Docker Compose
|
||||||
|
@echo "🛑 Stopping Docker Compose..."
|
||||||
|
@docker-compose down
|
||||||
|
|
||||||
|
k8s-deploy: docker-build ## Deploy to Kubernetes
|
||||||
|
@echo "☸️ Deploying to Kubernetes..."
|
||||||
|
@./scripts/deploy.sh
|
||||||
|
|
||||||
|
k8s-undeploy: ## Remove from Kubernetes
|
||||||
|
@echo "☸️ Removing from Kubernetes..."
|
||||||
|
@./scripts/undeploy.sh
|
||||||
|
|
||||||
deps: ## Download dependencies
|
deps: ## Download dependencies
|
||||||
@echo "📦 Downloading dependencies..."
|
@echo "📦 Downloading dependencies..."
|
||||||
@go mod download
|
@go mod download
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 🔐 Passkey Auth for Kubernetes Ingress Controllers
|
# 🔐 Passkey Auth for Kubernetes Nginx Ingress
|
||||||
|
|
||||||
A WebAuthn-based passkey authentication provider that integrates with ingress controllers. Currently supports Kubernetes Nginx Ingress Controller and Traefik Ingress Controller. Provides secure, passwordless authentication using passkeys (FIDO2/WebAuthn) as an auth backend.
|
A WebAuthn-based passkey authentication provider that integrates ingress controllers, currently support Kubernetes nginx Ingress controller. Provides secure, passwordless authentication using passkeys (FIDO2/WebAuthn) as an auth backend for nginx ingress.
|
||||||
|
|
||||||
## TLDR;
|
## TLDR;
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ I use it for signing into my home lab apps.
|
|||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **Passwordless Authentication**: Uses WebAuthn/FIDO2 passkeys for secure authentication
|
- **Passwordless Authentication**: Uses WebAuthn/FIDO2 passkeys for secure authentication
|
||||||
- **Ingress Controller Integration**: Works as auth backend for Nginx Ingress (`auth_request`) and Traefik Ingress (`ForwardAuth`)
|
- **Nginx Ingress Integration**: Works as auth backend using nginx `auth_request` directive
|
||||||
- **User Management**: An simple Admin interface for managing users and approval status
|
- **User Management**: An simple Admin interface for managing users and approval status
|
||||||
- **Kubernetes Native**: Designed for Kubernetes deployment with persistent storage
|
- **Kubernetes Native**: Designed for Kubernetes deployment with persistent storage
|
||||||
|
|
||||||
@@ -63,17 +63,15 @@ go run main.go
|
|||||||
|
|
||||||
### Setup Your App's Ingress
|
### Setup Your App's Ingress
|
||||||
|
|
||||||
#### Nginx Ingress Controller
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
name: your-app-ingress
|
name: your-app-ingress
|
||||||
annotations:
|
annotations:
|
||||||
|
|
||||||
nginx.ingress.kubernetes.io/auth-url: "https://your-passkey-auth.com/auth"
|
nginx.ingress.kubernetes.io/auth-url: "https://your-passkey-auth.com/auth"
|
||||||
nginx.ingress.kubernetes.io/auth-signin: "https://your-passkey-auth.com/?redirect=https%3A%2F%2F$host$request_uri"
|
nginx.ingress.kubernetes.io/auth-signin: "https://your-passkey-auth.com/?redirect=https%3A%2F%2F$host$request_uri"
|
||||||
nginx.ingress.kubernetes.io/auth-response-headers: "X-Auth-User,X-Auth-Email"
|
|
||||||
spec:
|
spec:
|
||||||
rules:
|
rules:
|
||||||
- host: your-app.com
|
- host: your-app.com
|
||||||
@@ -88,47 +86,6 @@ spec:
|
|||||||
number: 80
|
number: 80
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Traefik Ingress Controller
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: your-app-ingress
|
|
||||||
annotations:
|
|
||||||
traefik.ingress.kubernetes.io/router.middlewares: default-passkey-auth@kubernetescrd
|
|
||||||
spec:
|
|
||||||
rules:
|
|
||||||
- host: your-app.com
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: your-app-service
|
|
||||||
port:
|
|
||||||
number: 80
|
|
||||||
---
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: Middleware
|
|
||||||
metadata:
|
|
||||||
name: passkey-auth
|
|
||||||
spec:
|
|
||||||
forwardAuth:
|
|
||||||
address: https://your-passkey-auth.com/auth
|
|
||||||
authRequestHeaders:
|
|
||||||
- "X-Forwarded-Method"
|
|
||||||
- "X-Forwarded-Proto"
|
|
||||||
- "X-Forwarded-Host"
|
|
||||||
- "X-Forwarded-Uri"
|
|
||||||
- "X-Forwarded-For"
|
|
||||||
authResponseHeaders:
|
|
||||||
- "X-Auth-User"
|
|
||||||
- "X-Auth-Email"
|
|
||||||
authResponseHeadersRegex: "^X-"
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## 👥 User Management
|
## 👥 User Management
|
||||||
|
|
||||||
@@ -161,21 +118,6 @@ go run main.go
|
|||||||
# Access at http://localhost:8080
|
# Access at http://localhost:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
### Auth Endpoint Behavior
|
|
||||||
|
|
||||||
The `/auth` endpoint automatically adapts to work with both Nginx and Traefik ingress controllers:
|
|
||||||
|
|
||||||
**For Nginx auth_request:**
|
|
||||||
- Authenticated users: Returns `200 OK` with user headers
|
|
||||||
- Unauthenticated users: Returns `401 Unauthorized`
|
|
||||||
|
|
||||||
**For Traefik ForwardAuth:**
|
|
||||||
- Authenticated users: Returns `200 OK` with user headers
|
|
||||||
- Unauthenticated users (with redirect param): Returns `302 Found` with `Location` header pointing to login page
|
|
||||||
- Unauthenticated users (without redirect param): Returns `401 Unauthorized` (fallback for Nginx)
|
|
||||||
|
|
||||||
The endpoint detects the ingress controller type by checking for query parameters like `rd` or `redirect` that Traefik typically includes.
|
|
||||||
|
|
||||||
### Key API Endpoints
|
### Key API Endpoints
|
||||||
|
|
||||||
| Endpoint | Method | Description |
|
| Endpoint | Method | Description |
|
||||||
@@ -184,7 +126,7 @@ The endpoint detects the ingress controller type by checking for query parameter
|
|||||||
| `/api/register/finish` | POST | Complete passkey registration |
|
| `/api/register/finish` | POST | Complete passkey registration |
|
||||||
| `/api/login/begin` | POST | Start passkey authentication |
|
| `/api/login/begin` | POST | Start passkey authentication |
|
||||||
| `/api/login/finish` | POST | Complete passkey authentication |
|
| `/api/login/finish` | POST | Complete passkey authentication |
|
||||||
| `/auth` | GET | Auth check endpoint for ingress controllers |
|
| `/auth` | GET | Nginx auth check endpoint |
|
||||||
| `/api/users` | GET/POST | List/create users |
|
| `/api/users` | GET/POST | List/create users |
|
||||||
| `/health` | GET | Health check |
|
| `/health` | GET | Health check |
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: passkey-auth
|
name: passkey-auth
|
||||||
description: A WebAuthn-based passkey authentication provider for Kubernetes ingress controllers (Nginx & Traefik)
|
description: A WebAuthn-based passkey authentication provider for Kubernetes Nginx Ingress
|
||||||
type: application
|
type: application
|
||||||
version: 0.1.2
|
version: 0.1.1
|
||||||
appVersion: "main"
|
appVersion: "main"
|
||||||
home: https://github.com/wahyd4/passkey-auth
|
home: https://github.com/wahyd4/passkey-auth
|
||||||
sources:
|
sources:
|
||||||
@@ -15,7 +15,6 @@ keywords:
|
|||||||
- webauthn
|
- webauthn
|
||||||
- passkey
|
- passkey
|
||||||
- nginx-ingress
|
- nginx-ingress
|
||||||
- traefik-ingress
|
|
||||||
- security
|
- security
|
||||||
annotations:
|
annotations:
|
||||||
category: Security
|
category: Security
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Passkey Auth Helm Chart
|
# Passkey Auth Helm Chart
|
||||||
|
|
||||||
A Helm chart for deploying Passkey Auth, a WebAuthn-based passkey authentication provider that integrates with Kubernetes ingress controllers (Nginx Ingress Controller and Traefik Ingress Controller).
|
A Helm chart for deploying Passkey Auth, a WebAuthn-based passkey authentication provider that integrates with Kubernetes Nginx Ingress controller.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ helm upgrade --install my-passkey-auth -n home-apps -f my-values.yaml passkey-a
|
|||||||
|
|
||||||
- Kubernetes 1.19+
|
- Kubernetes 1.19+
|
||||||
- Helm 3.0+
|
- Helm 3.0+
|
||||||
- Ingress Controller (Nginx Ingress Controller or Traefik Ingress Controller)
|
- Nginx Ingress Controller
|
||||||
- StorageClass for persistent volumes
|
- StorageClass for persistent volumes
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -85,8 +85,6 @@ ingress:
|
|||||||
|
|
||||||
## Setup Authentication for Your Services
|
## Setup Authentication for Your Services
|
||||||
|
|
||||||
### Nginx Ingress Controller
|
|
||||||
|
|
||||||
Add these annotations to your ingress resources to protect them with passkey authentication:
|
Add these annotations to your ingress resources to protect them with passkey authentication:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -102,40 +100,6 @@ spec:
|
|||||||
# ... your ingress spec
|
# ... your ingress spec
|
||||||
```
|
```
|
||||||
|
|
||||||
### Traefik Ingress Controller
|
|
||||||
|
|
||||||
For Traefik, create a ForwardAuth middleware and reference it in your ingress:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: Middleware
|
|
||||||
metadata:
|
|
||||||
name: passkey-auth
|
|
||||||
namespace: your-app-namespace
|
|
||||||
spec:
|
|
||||||
forwardAuth:
|
|
||||||
address: https://auth.example.com/auth
|
|
||||||
authRequestHeaders:
|
|
||||||
- "X-Forwarded-Method"
|
|
||||||
- "X-Forwarded-Proto"
|
|
||||||
- "X-Forwarded-Host"
|
|
||||||
- "X-Forwarded-Uri"
|
|
||||||
- "X-Forwarded-For"
|
|
||||||
authResponseHeaders:
|
|
||||||
- "X-Auth-User"
|
|
||||||
- "X-Auth-Email"
|
|
||||||
authResponseHeadersRegex: "^X-"
|
|
||||||
---
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: my-protected-app
|
|
||||||
annotations:
|
|
||||||
traefik.ingress.kubernetes.io/router.middlewares: your-app-namespace-passkey-auth@kubernetescrd
|
|
||||||
spec:
|
|
||||||
# ... your ingress spec
|
|
||||||
```
|
|
||||||
|
|
||||||
## Advanced Configuration
|
## Advanced Configuration
|
||||||
|
|
||||||
### Custom Storage
|
### Custom Storage
|
||||||
@@ -292,11 +256,3 @@ secrets:
|
|||||||
| `nodeSelector` | Node labels for pod assignment | `{}` |
|
| `nodeSelector` | Node labels for pod assignment | `{}` |
|
||||||
| `tolerations` | Tolerations for pod assignment | `[]` |
|
| `tolerations` | Tolerations for pod assignment | `[]` |
|
||||||
| `affinity` | Affinity for pod assignment | `{}` |
|
| `affinity` | Affinity for pod assignment | `{}` |
|
||||||
|
|
||||||
### Traefik configuration
|
|
||||||
|
|
||||||
| Name | Description | Value |
|
|
||||||
| ------------------------------ | --------------------------------------------------------------- | ------- |
|
|
||||||
| `traefik.enabled` | Enable Traefik-specific features | `false` |
|
|
||||||
| `traefik.middleware.create` | Create Traefik ForwardAuth middleware | `true` |
|
|
||||||
| `traefik.middleware.name` | Custom name for middleware (defaults to chart fullname) | `""` |
|
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
# Example values for Traefik deployment
|
|
||||||
# Copy this file and modify for your environment
|
|
||||||
|
|
||||||
config:
|
|
||||||
webauthn:
|
|
||||||
rpId: "auth.example.com"
|
|
||||||
rpOrigins:
|
|
||||||
- "https://auth.example.com"
|
|
||||||
|
|
||||||
cors:
|
|
||||||
allowedOrigins:
|
|
||||||
- "https://*.example.com"
|
|
||||||
|
|
||||||
auth:
|
|
||||||
cookieDomain: ".example.com"
|
|
||||||
allowedEmails:
|
|
||||||
- "admin@example.com"
|
|
||||||
|
|
||||||
secrets:
|
|
||||||
sessionSecret: "your-secure-random-secret-min-32-chars"
|
|
||||||
|
|
||||||
ingress:
|
|
||||||
enabled: true
|
|
||||||
className: "traefik"
|
|
||||||
annotations:
|
|
||||||
kubernetes.io/tls-acme: "true"
|
|
||||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
|
||||||
hosts:
|
|
||||||
- host: auth.example.com
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
tls:
|
|
||||||
- secretName: passkey-auth-tls
|
|
||||||
hosts:
|
|
||||||
- auth.example.com
|
|
||||||
|
|
||||||
# Enable Traefik middleware creation
|
|
||||||
traefik:
|
|
||||||
enabled: true
|
|
||||||
middleware:
|
|
||||||
create: true
|
|
||||||
name: "passkey-auth" # Optional: custom name
|
|
||||||
|
|
||||||
persistence:
|
|
||||||
enabled: true
|
|
||||||
size: 2Gi
|
|
||||||
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 400m
|
|
||||||
memory: 512Mi
|
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 128Mi
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{{- if and .Values.traefik.enabled .Values.traefik.middleware.create }}
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: Middleware
|
|
||||||
metadata:
|
|
||||||
name: {{ default (include "passkey-auth.fullname" .) .Values.traefik.middleware.name }}-forwardauth
|
|
||||||
namespace: {{ .Release.Namespace }}
|
|
||||||
labels:
|
|
||||||
{{- include "passkey-auth.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
forwardAuth:
|
|
||||||
address: http{{ if .Values.ingress.tls }}s{{ end }}://{{ (index .Values.ingress.hosts 0).host }}/auth
|
|
||||||
authRequestHeaders:
|
|
||||||
- X-Forwarded-Method
|
|
||||||
- X-Forwarded-Proto
|
|
||||||
- X-Forwarded-Host
|
|
||||||
- X-Forwarded-Uri
|
|
||||||
- X-Forwarded-For
|
|
||||||
- Cookie
|
|
||||||
- Authorization
|
|
||||||
authResponseHeaders:
|
|
||||||
- X-Auth-User
|
|
||||||
- X-Auth-Email
|
|
||||||
authResponseHeadersRegex: "^X-"
|
|
||||||
authResponseHeaders:
|
|
||||||
- Location
|
|
||||||
authResponseHeadersRegex: ^X-|^Location$
|
|
||||||
{{- end }}
|
|
||||||
@@ -166,10 +166,3 @@ volumeMounts: []
|
|||||||
|
|
||||||
# Additional volumes
|
# Additional volumes
|
||||||
volumes: []
|
volumes: []
|
||||||
|
|
||||||
# Traefik-specific configuration
|
|
||||||
traefik:
|
|
||||||
enabled: false
|
|
||||||
middleware:
|
|
||||||
create: true
|
|
||||||
name: "" # If empty, will use chart fullname
|
|
||||||
|
|||||||
@@ -392,18 +392,17 @@ func (h *Handlers) Logout(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.writeJSON(w, map[string]string{"status": "success"})
|
h.writeJSON(w, map[string]string{"status": "success"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthCheck implements auth backend for both nginx auth_request and Traefik ForwardAuth
|
// AuthCheck implements the nginx auth_request protocol
|
||||||
func (h *Handlers) AuthCheck(w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) AuthCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
// Debug logging
|
// Debug logging
|
||||||
logrus.Debugf("AuthCheck request from %s", r.RemoteAddr)
|
logrus.Debugf("AuthCheck request from %s", r.RemoteAddr)
|
||||||
logrus.Debugf("AuthCheck headers: %+v", r.Header)
|
logrus.Debugf("AuthCheck headers: %+v", r.Header)
|
||||||
logrus.Debugf("AuthCheck cookies: %+v", r.Cookies())
|
logrus.Debugf("AuthCheck cookies: %+v", r.Cookies())
|
||||||
logrus.Debugf("AuthCheck query params: %+v", r.URL.Query())
|
|
||||||
|
|
||||||
session, err := h.store.Get(r, "auth-session")
|
session, err := h.store.Get(r, "auth-session")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("Failed to get auth session: %v", err)
|
logrus.Errorf("Failed to get auth session: %v", err)
|
||||||
h.handleUnauthenticated(w, r)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,8 +411,8 @@ func (h *Handlers) AuthCheck(w http.ResponseWriter, r *http.Request) {
|
|||||||
logrus.Debugf("Session values: %+v", session.Values)
|
logrus.Debugf("Session values: %+v", session.Values)
|
||||||
|
|
||||||
if !ok || !authenticated {
|
if !ok || !authenticated {
|
||||||
logrus.Debugf("User not authenticated")
|
logrus.Debugf("User not authenticated, returning 401")
|
||||||
h.handleUnauthenticated(w, r)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,41 +428,6 @@ func (h *Handlers) AuthCheck(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleUnauthenticated handles unauthenticated requests for both Nginx and Traefik
|
|
||||||
func (h *Handlers) handleUnauthenticated(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Check for redirect parameter (Traefik ForwardAuth typically includes this)
|
|
||||||
redirectURL := r.URL.Query().Get("rd")
|
|
||||||
if redirectURL == "" {
|
|
||||||
// Also check for other common redirect parameter names
|
|
||||||
redirectURL = r.URL.Query().Get("redirect")
|
|
||||||
}
|
|
||||||
|
|
||||||
// If redirect parameter is present, return 302 redirect (Traefik ForwardAuth)
|
|
||||||
if redirectURL != "" {
|
|
||||||
logrus.Debugf("Redirect parameter found (%s), returning 302 redirect for Traefik", redirectURL)
|
|
||||||
|
|
||||||
// Construct login URL with redirect parameter
|
|
||||||
loginURL := "/login.html?redirect=" + redirectURL
|
|
||||||
|
|
||||||
// If we have a host header, construct a full URL
|
|
||||||
if host := r.Header.Get("Host"); host != "" {
|
|
||||||
scheme := "http"
|
|
||||||
if r.Header.Get("X-Forwarded-Proto") == "https" || r.TLS != nil {
|
|
||||||
scheme = "https"
|
|
||||||
}
|
|
||||||
loginURL = scheme + "://" + host + loginURL
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Location", loginURL)
|
|
||||||
w.WriteHeader(http.StatusFound) // 302
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// No redirect parameter, return 401 (Nginx auth_request)
|
|
||||||
logrus.Debugf("No redirect parameter, returning 401 for Nginx auth_request")
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthStatus returns the current authentication status
|
// GetAuthStatus returns the current authentication status
|
||||||
func (h *Handlers) GetAuthStatus(w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) GetAuthStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
session, err := h.store.Get(r, "auth-session")
|
session, err := h.store.Get(r, "auth-session")
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"passkey-auth/internal/auth"
|
|
||||||
"passkey-auth/internal/config"
|
|
||||||
"passkey-auth/internal/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAuthCheck(t *testing.T) {
|
|
||||||
// Setup test config
|
|
||||||
cfg := &config.Config{
|
|
||||||
Auth: config.AuthConfig{
|
|
||||||
SessionSecret: "test-secret",
|
|
||||||
},
|
|
||||||
WebAuthn: config.WebAuthnConfig{
|
|
||||||
RPDisplayName: "Test Passkey Auth",
|
|
||||||
RPID: "localhost",
|
|
||||||
RPOrigins: []string{"http://localhost:8080"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup test database
|
|
||||||
db, err := database.New(":memory:")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test database: %v", err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Setup test WebAuthn (mock)
|
|
||||||
webAuthn, err := auth.NewWebAuthn(cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test WebAuthn: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create handlers
|
|
||||||
h := New(db, webAuthn, cfg)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
queryParams string
|
|
||||||
authenticated bool
|
|
||||||
expectedStatus int
|
|
||||||
expectedHeader string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Unauthenticated without redirect param (Nginx)",
|
|
||||||
queryParams: "",
|
|
||||||
authenticated: false,
|
|
||||||
expectedStatus: http.StatusUnauthorized,
|
|
||||||
expectedHeader: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Unauthenticated with rd param (Traefik)",
|
|
||||||
queryParams: "rd=https://example.com/protected",
|
|
||||||
authenticated: false,
|
|
||||||
expectedStatus: http.StatusFound,
|
|
||||||
expectedHeader: "/login.html?redirect=https://example.com/protected",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Unauthenticated with redirect param (Traefik)",
|
|
||||||
queryParams: "redirect=https://example.com/protected",
|
|
||||||
authenticated: false,
|
|
||||||
expectedStatus: http.StatusFound,
|
|
||||||
expectedHeader: "/login.html?redirect=https://example.com/protected",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Authenticated with redirect param",
|
|
||||||
queryParams: "rd=https://example.com/protected",
|
|
||||||
authenticated: true,
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedHeader: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Authenticated without redirect param",
|
|
||||||
queryParams: "",
|
|
||||||
authenticated: true,
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedHeader: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Create request
|
|
||||||
req, err := http.NewRequest("GET", "/auth?"+tt.queryParams, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock session if authenticated
|
|
||||||
if tt.authenticated {
|
|
||||||
// Create a session cookie for testing
|
|
||||||
session, _ := h.store.Get(req, "auth-session")
|
|
||||||
session.Values["authenticated"] = true
|
|
||||||
session.Values["user_id"] = 1
|
|
||||||
session.Values["user_email"] = "test@example.com"
|
|
||||||
|
|
||||||
// Create a response recorder to capture the session cookie
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
session.Save(req, w)
|
|
||||||
|
|
||||||
// Extract the cookie and add it to the request
|
|
||||||
for _, cookie := range w.Result().Cookies() {
|
|
||||||
if cookie.Name == "auth-session" {
|
|
||||||
req.AddCookie(cookie)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create response recorder
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Call the handler
|
|
||||||
h.AuthCheck(w, req)
|
|
||||||
|
|
||||||
// Check status code
|
|
||||||
if w.Code != tt.expectedStatus {
|
|
||||||
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Location header for redirects
|
|
||||||
if tt.expectedStatus == http.StatusFound {
|
|
||||||
location := w.Header().Get("Location")
|
|
||||||
if location != tt.expectedHeader {
|
|
||||||
t.Errorf("Expected Location header %s, got %s", tt.expectedHeader, location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check auth headers for authenticated requests
|
|
||||||
if tt.authenticated && tt.expectedStatus == http.StatusOK {
|
|
||||||
userHeader := w.Header().Get("X-Auth-User")
|
|
||||||
if userHeader == "" {
|
|
||||||
t.Error("Expected X-Auth-User header for authenticated request")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuthCheckWithHost(t *testing.T) {
|
|
||||||
// Setup test config
|
|
||||||
cfg := &config.Config{
|
|
||||||
Auth: config.AuthConfig{
|
|
||||||
SessionSecret: "test-secret",
|
|
||||||
},
|
|
||||||
WebAuthn: config.WebAuthnConfig{
|
|
||||||
RPDisplayName: "Test Passkey Auth",
|
|
||||||
RPID: "localhost",
|
|
||||||
RPOrigins: []string{"http://localhost:8080"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup test database
|
|
||||||
db, err := database.New(":memory:")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test database: %v", err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Setup test WebAuthn
|
|
||||||
webAuthn, err := auth.NewWebAuthn(cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test WebAuthn: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create handlers
|
|
||||||
h := New(db, webAuthn, cfg)
|
|
||||||
|
|
||||||
// Test with Host header
|
|
||||||
req, err := http.NewRequest("GET", "/auth?rd=https://example.com/protected", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create request: %v", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Host", "auth.example.com")
|
|
||||||
req.Header.Set("X-Forwarded-Proto", "https")
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
h.AuthCheck(w, req)
|
|
||||||
|
|
||||||
// Should return 302 with full URL
|
|
||||||
if w.Code != http.StatusFound {
|
|
||||||
t.Errorf("Expected status %d, got %d", http.StatusFound, w.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedLocation := "https://auth.example.com/login.html?redirect=https://example.com/protected"
|
|
||||||
location := w.Header().Get("Location")
|
|
||||||
if location != expectedLocation {
|
|
||||||
t.Errorf("Expected Location header %s, got %s", expectedLocation, location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -61,9 +61,7 @@ func main() {
|
|||||||
api.HandleFunc("/users/{id}", h.UpdateUser).Methods("PUT")
|
api.HandleFunc("/users/{id}", h.UpdateUser).Methods("PUT")
|
||||||
api.HandleFunc("/users/{id}", h.DeleteUser).Methods("DELETE")
|
api.HandleFunc("/users/{id}", h.DeleteUser).Methods("DELETE")
|
||||||
|
|
||||||
// Auth backend endpoint for ingress controllers (Nginx auth_request & Traefik ForwardAuth)
|
// Nginx auth backend endpoint
|
||||||
// - For Nginx: Returns 200 for authenticated, 401 for unauthenticated
|
|
||||||
// - For Traefik: Returns 200 for authenticated, 302 redirect for unauthenticated (when rd param present)
|
|
||||||
router.HandleFunc("/auth", h.AuthCheck).Methods("GET", "HEAD")
|
router.HandleFunc("/auth", h.AuthCheck).Methods("GET", "HEAD")
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
|
|||||||
Reference in New Issue
Block a user