feat: forward auth

This adds basic support for forwarded authentication similar to caddy and traefik. This replaces Authelia SSO as it effectively covers exactly the same use cases.
This commit is contained in:
James Elliott 2025-04-21 10:50:37 +10:00
parent 0e5550487e
commit 8f046a0b47
No known key found for this signature in database
GPG Key ID: 0F1C4A096E857E49
14 changed files with 433 additions and 490 deletions

View File

@ -80,10 +80,9 @@ func RegisterTLSAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/cert/delete", handleCertRemove) authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
} }
// Register the APIs for Authentication handlers like Authelia and OAUTH2 // Register the APIs for Authentication handlers like Forward Auth and OAUTH2
func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) { func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/sso/Authelia", autheliaRouter.HandleSetAutheliaURLAndHTTPS) authRouter.HandleFunc("/api/sso/forward-auth", forwardAuthRouter.HandleAPIOptions)
authRouter.HandleFunc("/api/sso/Authentik", authentikRouter.HandleSetAuthentikURLAndHTTPS)
} }
// Register the APIs for redirection rules management functions // Register the APIs for redirection rules management functions

View File

@ -13,12 +13,10 @@ import (
"net/http" "net/http"
"time" "time"
"imuslab.com/zoraxy/mod/auth/sso/authentik"
"imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/auth/sso/authelia" "imuslab.com/zoraxy/mod/auth/sso/forward"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dockerux" "imuslab.com/zoraxy/mod/dockerux"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
@ -43,8 +41,9 @@ import (
const ( const (
/* Build Constants */ /* Build Constants */
SYSTEM_NAME = "Zoraxy" SYSTEM_NAME = "Zoraxy"
SYSTEM_VERSION = "3.2.1" SYSTEM_VERSION = "3.2.2"
DEVELOPMENT_BUILD = false
/* System Constants */ /* System Constants */
TMP_FOLDER = "./tmp" TMP_FOLDER = "./tmp"
@ -144,8 +143,7 @@ var (
pluginManager *plugins.Manager //Plugin manager for managing plugins pluginManager *plugins.Manager //Plugin manager for managing plugins
//Authentication Provider //Authentication Provider
autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication forwardAuthRouter *forward.AuthRouter // Forward Auth router for Authelia/Authentik/etc authentication
authentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication
//Helper modules //Helper modules
EmailSender *email.Sender //Email sender that handle email sending EmailSender *email.Sender //Email sender that handle email sending

View File

@ -1,164 +0,0 @@
package authelia
import (
"encoding/json"
"errors"
"net"
"net/http"
"net/url"
"strings"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
type AutheliaRouterOptions struct {
UseHTTPS bool //If the Authelia server is using HTTPS
AutheliaURL string //The URL of the Authelia server
Logger *logger.Logger
Database *database.Database
}
type AutheliaRouter struct {
options *AutheliaRouterOptions
}
// NewAutheliaRouter creates a new AutheliaRouter object
func NewAutheliaRouter(options *AutheliaRouterOptions) *AutheliaRouter {
options.Database.NewTable("authelia")
//Read settings from database, if exists
options.Database.Read("authelia", "autheliaURL", &options.AutheliaURL)
options.Database.Read("authelia", "useHTTPS", &options.UseHTTPS)
return &AutheliaRouter{
options: options,
}
}
// HandleSetAutheliaURLAndHTTPS is the internal handler for setting the Authelia URL and HTTPS
func (ar *AutheliaRouter) HandleSetAutheliaURLAndHTTPS(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current settings
js, _ := json.Marshal(map[string]interface{}{
"useHTTPS": ar.options.UseHTTPS,
"autheliaURL": ar.options.AutheliaURL,
})
utils.SendJSONResponse(w, string(js))
return
} else if r.Method == http.MethodPost {
//Update the settings
autheliaURL, err := utils.PostPara(r, "autheliaURL")
if err != nil {
utils.SendErrorResponse(w, "autheliaURL not found")
return
}
useHTTPS, err := utils.PostBool(r, "useHTTPS")
if err != nil {
useHTTPS = false
}
//Write changes to runtime
ar.options.AutheliaURL = autheliaURL
ar.options.UseHTTPS = useHTTPS
//Write changes to database
ar.options.Database.Write("authelia", "autheliaURL", autheliaURL)
ar.options.Database.Write("authelia", "useHTTPS", useHTTPS)
utils.SendOK(w)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
}
// handleAutheliaAuth is the internal handler for Authelia authentication
// Set useHTTPS to true if your authelia server is using HTTPS
// Set autheliaURL to the URL of the Authelia server, e.g. authelia.example.com
func (ar *AutheliaRouter) HandleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
client := &http.Client{}
if ar.options.AutheliaURL == "" {
ar.options.Logger.PrintAndLog("Authelia", "Authelia URL not set", nil)
w.WriteHeader(500)
w.Write([]byte("500 - Internal Server Error"))
return errors.New("authelia URL not set")
}
protocol := "http"
if ar.options.UseHTTPS {
protocol = "https"
}
autheliaURL := &url.URL{
Scheme: protocol,
Host: ar.options.AutheliaURL,
}
//Make a request to Authelia to verify the request
req, err := http.NewRequest("POST", autheliaURL.JoinPath("api", "verify").String(), nil)
if err != nil {
ar.options.Logger.PrintAndLog("Authelia", "Unable to create request", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
originalURL := rOriginalHeaders(r, req)
// Copy cookies from the incoming request
for _, cookie := range r.Cookies() {
req.AddCookie(cookie)
}
// Making the verification request
resp, err := client.Do(req)
if err != nil {
ar.options.Logger.PrintAndLog("Authelia", "Unable to verify", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
if resp.StatusCode != 200 {
redirectURL := autheliaURL.JoinPath()
query := redirectURL.Query()
query.Set("rd", originalURL.String())
query.Set("rm", r.Method)
http.Redirect(w, r, redirectURL.String(), http.StatusSeeOther)
return errors.New("unauthorized")
}
return nil
}
func rOriginalHeaders(r, req *http.Request) *url.URL {
if r.RemoteAddr != "" {
before, _, _ := strings.Cut(r.RemoteAddr, ":")
if ip := net.ParseIP(before); ip != nil {
req.Header.Set("X-Forwarded-For", ip.String())
}
}
originalURL := &url.URL{
Scheme: "http",
Host: r.Host,
Path: r.URL.Path,
RawPath: r.URL.RawPath,
}
if r.TLS != nil {
originalURL.Scheme = "https"
}
req.Header.Add("X-Forwarded-Method", r.Method)
req.Header.Add("X-Original-URL", originalURL.String())
return originalURL
}

View File

@ -1,169 +0,0 @@
package authentik
import (
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"strings"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
type AuthentikRouterOptions struct {
UseHTTPS bool //If the Authentik server is using HTTPS
AuthentikURL string //The URL of the Authentik server
Logger *logger.Logger
Database *database.Database
}
type AuthentikRouter struct {
options *AuthentikRouterOptions
}
// NewAuthentikRouter creates a new AuthentikRouter object
func NewAuthentikRouter(options *AuthentikRouterOptions) *AuthentikRouter {
options.Database.NewTable("authentik")
//Read settings from database, if exists
options.Database.Read("authentik", "authentikURL", &options.AuthentikURL)
options.Database.Read("authentik", "useHTTPS", &options.UseHTTPS)
return &AuthentikRouter{
options: options,
}
}
// HandleSetAuthentikURLAndHTTPS is the internal handler for setting the Authentik URL and HTTPS
func (ar *AuthentikRouter) HandleSetAuthentikURLAndHTTPS(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current settings
js, _ := json.Marshal(map[string]interface{}{
"useHTTPS": ar.options.UseHTTPS,
"authentikURL": ar.options.AuthentikURL,
})
utils.SendJSONResponse(w, string(js))
return
} else if r.Method == http.MethodPost {
//Update the settings
AuthentikURL, err := utils.PostPara(r, "authentikURL")
if err != nil {
utils.SendErrorResponse(w, "authentikURL not found")
return
}
useHTTPS, err := utils.PostBool(r, "useHTTPS")
if err != nil {
useHTTPS = false
}
//Write changes to runtime
ar.options.AuthentikURL = AuthentikURL
ar.options.UseHTTPS = useHTTPS
//Write changes to database
ar.options.Database.Write("authentik", "authentikURL", AuthentikURL)
ar.options.Database.Write("authentik", "useHTTPS", useHTTPS)
utils.SendOK(w)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
}
// HandleAuthentikAuth is the internal handler for Authentik authentication
// Set useHTTPS to true if your Authentik server is using HTTPS
// Set AuthentikURL to the URL of the Authentik server, e.g. Authentik.example.com
func (ar *AuthentikRouter) HandleAuthentikAuth(w http.ResponseWriter, r *http.Request) error {
const outpostPrefix = "outpost.goauthentik.io"
client := &http.Client{}
if ar.options.AuthentikURL == "" {
ar.options.Logger.PrintAndLog("Authentik", "Authentik URL not set", nil)
w.WriteHeader(500)
w.Write([]byte("500 - Internal Server Error"))
return errors.New("authentik URL not set")
}
protocol := "http"
if ar.options.UseHTTPS {
protocol = "https"
}
authentikBaseURL := protocol + "://" + ar.options.AuthentikURL
//Remove tailing slash if any
authentikBaseURL = strings.TrimSuffix(authentikBaseURL, "/")
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
reqUrl := scheme + "://" + r.Host + r.RequestURI
// Pass request to outpost if path matches outpost prefix
if reqPath := strings.TrimPrefix(r.URL.Path, "/"); strings.HasPrefix(reqPath, outpostPrefix) {
req, err := http.NewRequest(r.Method, authentikBaseURL+r.RequestURI, r.Body)
if err != nil {
ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
req.Header.Set("X-Original-URL", reqUrl)
req.Header.Set("Host", r.Host)
for _, cookie := range r.Cookies() {
req.AddCookie(cookie)
}
if resp, err := client.Do(req); err != nil {
ar.options.Logger.PrintAndLog("Authentik", "Unable to pass request to Authentik outpost", err)
w.WriteHeader(http.StatusInternalServerError)
return errors.New("internal server error")
} else {
defer resp.Body.Close()
for k := range resp.Header {
w.Header().Set(k, resp.Header.Get(k))
}
w.WriteHeader(resp.StatusCode)
if _, err = io.Copy(w, resp.Body); err != nil {
ar.options.Logger.PrintAndLog("Authentik", "Unable to pass Authentik outpost response to client", err)
w.WriteHeader(http.StatusInternalServerError)
return errors.New("internal server error")
}
}
return nil
}
//Make a request to Authentik to verify the request
req, err := http.NewRequest(http.MethodGet, authentikBaseURL+"/"+outpostPrefix+"/auth/nginx", nil)
if err != nil {
ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
req.Header.Set("X-Original-URL", reqUrl)
// Copy cookies from the incoming request
for _, cookie := range r.Cookies() {
req.AddCookie(cookie)
}
// Making the verification request
resp, err := client.Do(req)
if err != nil {
ar.options.Logger.PrintAndLog("Authentik", "Unable to verify", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
if resp.StatusCode != 200 {
redirectURL := authentikBaseURL + "/" + outpostPrefix + "/start?rd=" + url.QueryEscape(scheme+"://"+r.Host+r.URL.String())
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return errors.New("unauthorized")
}
return nil
}

View File

@ -0,0 +1,44 @@
package forward
import "errors"
const (
LogTitle = "Forward Auth"
DatabaseTable = "auth_sso_forward"
DatabaseKeyAddress = "address"
DatabaseKeyResponseHeaders = "responseHeaders"
DatabaseKeyRequestExcludedCookies = "requestExcludedCookies"
HeaderXForwardedProto = "X-Forwarded-Proto"
HeaderXForwardedHost = "X-Forwarded-Host"
HeaderXForwardedFor = "X-Forwarded-For"
HeaderXForwardedURI = "X-Forwarded-URI"
HeaderXForwardedMethod = "X-Forwarded-Method"
HeaderCookie = "Cookie"
HeaderUpgrade = "Upgrade"
HeaderConnection = "Connection"
HeaderTransferEncoding = "Transfer-Encoding"
HeaderTE = "TE"
HeaderTrailers = "Trailers"
HeaderKeepAlive = "Keep-Alive"
)
var (
ErrInternalServerError = errors.New("internal server error")
ErrUnauthorized = errors.New("unauthorized")
)
var (
doNotCopyHeaders = []string{
HeaderUpgrade,
HeaderConnection,
HeaderTransferEncoding,
HeaderTE,
HeaderTrailers,
HeaderKeepAlive,
}
)

View File

@ -0,0 +1,294 @@
package forward
import (
"encoding/json"
"io"
"net"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
type AuthRouterOptions struct {
// Address of the forward auth endpoint.
Address string
// ResponseHeaders is a list of headers to be copied from the response if provided by the forward auth endpoint to
// the request.
ResponseHeaders []string
// RequestExcludedCookies is a list of cookie keys that should be removed from every request sent to the upstream.
RequestExcludedCookies []string
Logger *logger.Logger
Database *database.Database
}
type AuthRouter struct {
client *http.Client
options *AuthRouterOptions
}
// NewAuthRouter creates a new AuthRouter object
func NewAuthRouter(options *AuthRouterOptions) *AuthRouter {
options.Database.NewTable(DatabaseTable)
//Read settings from database if available.
options.Database.Read(DatabaseTable, DatabaseKeyAddress, &options.Address)
responseHeaders, requestExcludedCookies := "", ""
options.Database.Read(DatabaseTable, DatabaseKeyResponseHeaders, responseHeaders)
options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, requestExcludedCookies)
options.ResponseHeaders = strings.Split(responseHeaders, ",")
options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
return &AuthRouter{
client: &http.Client{
CheckRedirect: func(r *http.Request, via []*http.Request) (err error) {
return http.ErrUseLastResponse
},
},
options: options,
}
}
// HandleAPIOptions is the internal handler for setting the options.
func (ar *AuthRouter) HandleAPIOptions(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
ar.handleOptionsGET(w, r)
case http.MethodPost:
ar.handleOptionsPOST(w, r)
default:
ar.handleOptionsMethodNotAllowed(w, r)
}
}
func (ar *AuthRouter) handleOptionsGET(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(map[string]interface{}{
DatabaseKeyAddress: ar.options.Address,
DatabaseKeyResponseHeaders: ar.options.ResponseHeaders,
DatabaseKeyRequestExcludedCookies: ar.options.RequestExcludedCookies,
})
utils.SendJSONResponse(w, string(js))
return
}
func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request) {
// Update the settings
address, err := utils.PostPara(r, DatabaseKeyAddress)
if err != nil {
utils.SendErrorResponse(w, "address not found")
return
}
// These are optional fields.
responseHeaders, _ := utils.PostPara(r, DatabaseKeyResponseHeaders)
requestExcludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestExcludedCookies)
// Write changes to runtime
ar.options.Address = address
ar.options.ResponseHeaders = strings.Split(responseHeaders, ",")
ar.options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
// Write changes to database
ar.options.Database.Write(DatabaseTable, DatabaseKeyAddress, address)
ar.options.Database.Write(DatabaseTable, DatabaseKeyResponseHeaders, responseHeaders)
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestExcludedCookies, requestExcludedCookies)
utils.SendOK(w)
}
func (ar *AuthRouter) handleOptionsMethodNotAllowed(w http.ResponseWriter, r *http.Request) {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
// HandleAuthProviderRouting is the internal handler for Forward Auth authentication.
func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.Request) error {
if ar.options.Address == "" {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Address not set", nil)
return ErrInternalServerError
}
// Make a request to Authz Server to verify the request
req, err := http.NewRequest(http.MethodGet, ar.options.Address, nil)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Unable to create request", err)
return ErrInternalServerError
}
// TODO: Add opt-in support for copying the request body to the forward auth request.
// TODO: Add support for customizing which headers are copied from the request to the forward auth request.
headerCopyExcluded(r.Header, req.Header, nil)
// TODO: Add support for upstream headers.
rSetForwardedHeaders(r, req)
// Make the Authz Request.
respForwarded, err := ar.client.Do(req)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Unable to perform forwarded auth due to a request error", err)
return ErrInternalServerError
}
defer respForwarded.Body.Close()
body, err := io.ReadAll(respForwarded.Body)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Unable to read response to forward auth request", err)
return ErrInternalServerError
}
// Responses within the 200-299 range are considered successful and allow the proxy to handle the request.
if respForwarded.StatusCode >= http.StatusOK && respForwarded.StatusCode < http.StatusMultipleChoices {
// TODO: Add support for copying response headers to the response (in the user agent), not just the request.
if len(ar.options.ResponseHeaders) != 0 {
// If the user has specified a list of cookies to be removed from the request, deterministically remove them.
headerCookieRedact(r, ar.options.RequestExcludedCookies)
}
// Copy specific user-specified headers from the response of the forward auth request to the request sent to the
// upstream server/next hop.
headerCopyIncluded(respForwarded.Header, w.Header(), ar.options.ResponseHeaders)
return nil
}
// Copy the response.
headerCopyExcluded(respForwarded.Header, w.Header(), nil)
w.WriteHeader(respForwarded.StatusCode)
if _, err = w.Write(body); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Unable to write response", err)
return ErrInternalServerError
}
return ErrUnauthorized
}
func scheme(r *http.Request) string {
if r.TLS != nil {
return "https"
}
return "http"
}
func headerCookieRedact(r *http.Request, excluded []string) {
original := r.Cookies()
if len(original) == 0 {
return
}
var cookies []string
for _, cookie := range original {
if stringInSlice(cookie.Name, excluded) {
continue
}
cookies = append(cookies, cookie.String())
}
r.Header.Set(HeaderCookie, strings.Join(cookies, "; "))
}
func headerCopyExcluded(original, destination http.Header, excludedHeaders []string) {
for key, values := range original {
// We should never copy the headers in the below list.
if stringInSliceFold(key, doNotCopyHeaders) {
continue
}
if stringInSliceFold(key, excludedHeaders) {
continue
}
destination[key] = append(destination[key], values...)
}
}
func headerCopyIncluded(original, destination http.Header, includedHeaders []string) {
for key, values := range original {
// We should never copy the headers in the below list, even if they're in the list provided by a user.
if stringInSliceFold(key, doNotCopyHeaders) {
continue
}
if !stringInSliceFold(key, includedHeaders) {
continue
}
destination[key] = append(destination[key], values...)
}
}
func stringInSlice(needle string, haystack []string) bool {
if len(haystack) == 0 {
return false
}
for _, v := range haystack {
if needle == v {
return true
}
}
return false
}
func stringInSliceFold(needle string, haystack []string) bool {
if len(haystack) == 0 {
return false
}
for _, v := range haystack {
if strings.EqualFold(needle, v) {
return true
}
}
return false
}
func rSetForwardedHeaders(r, req *http.Request) {
if r.RemoteAddr != "" {
before, _, _ := strings.Cut(r.RemoteAddr, ":")
if ip := net.ParseIP(before); ip != nil {
req.Header.Set(HeaderXForwardedFor, ip.String())
}
}
req.Header.Set(HeaderXForwardedMethod, r.Method)
req.Header.Set(HeaderXForwardedProto, scheme(r))
req.Header.Set(HeaderXForwardedHost, r.Host)
req.Header.Set(HeaderXForwardedURI, r.URL.Path)
}

View File

@ -32,20 +32,16 @@ and return a boolean indicate if the request is written to http.ResponseWriter
*/ */
func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *http.Request, h *ProxyHandler) bool { func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *http.Request, h *ProxyHandler) bool {
requestHostname := r.Host requestHostname := r.Host
if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic {
switch sep.AuthenticationProvider.AuthMethod {
case AuthMethodBasic:
err := h.handleBasicAuthRouting(w, r, sep) err := h.handleBasicAuthRouting(w, r, sep)
if err != nil { if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "") h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
return true return true
} }
} else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthelia { case AuthMethodForward:
err := h.handleAutheliaAuth(w, r) err := h.handleForwardAuth(w, r)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
return true
}
} else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthentik {
err := h.handleAuthentikAuth(w, r)
if err != nil { if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "") h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
return true return true
@ -106,13 +102,9 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
return nil return nil
} }
/* Authelia */ /* Forward Auth */
// Handle authelia auth routing // Handle forward auth routing
func (h *ProxyHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Request) error { func (h *ProxyHandler) handleForwardAuth(w http.ResponseWriter, r *http.Request) error {
return h.Parent.Option.AutheliaRouter.HandleAutheliaAuth(w, r) return h.Parent.Option.ForwardAuthRouter.HandleAuthProviderRouting(w, r)
}
func (h *ProxyHandler) handleAuthentikAuth(w http.ResponseWriter, r *http.Request) error {
return h.Parent.Option.AuthentikRouter.HandleAuthentikAuth(w, r)
} }

View File

@ -17,12 +17,13 @@ import (
// GetDefaultAuthenticationProvider return a default authentication provider // GetDefaultAuthenticationProvider return a default authentication provider
func GetDefaultAuthenticationProvider() *AuthenticationProvider { func GetDefaultAuthenticationProvider() *AuthenticationProvider {
return &AuthenticationProvider{ return &AuthenticationProvider{
AuthMethod: AuthMethodNone, AuthMethod: AuthMethodNone,
BasicAuthCredentials: []*BasicAuthCredentials{}, BasicAuthCredentials: []*BasicAuthCredentials{},
BasicAuthExceptionRules: []*BasicAuthExceptionRule{}, BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
BasicAuthGroupIDs: []string{}, BasicAuthGroupIDs: []string{},
AutheliaURL: "", ForwardAuthURL: "",
UseHTTPS: false, ForwardAuthResponseHeaders: []string{},
ForwardAuthRequestExcludedCookies: []string{},
} }
} }

View File

@ -9,13 +9,12 @@ package dynamicproxy
*/ */
import ( import (
_ "embed" _ "embed"
"imuslab.com/zoraxy/mod/auth/sso/authentik"
"net" "net"
"net/http" "net/http"
"sync" "sync"
"imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/auth/sso/authelia" "imuslab.com/zoraxy/mod/auth/sso/forward"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
@ -64,8 +63,7 @@ type RouterOption struct {
PluginManager *plugins.Manager //Plugin manager for handling plugin routing PluginManager *plugins.Manager //Plugin manager for handling plugin routing
/* Authentication Providers */ /* Authentication Providers */
AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication ForwardAuthRouter *forward.AuthRouter
AuthentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication
/* Utilities */ /* Utilities */
Logger *logger.Logger //Logger for reverse proxy requets Logger *logger.Logger //Logger for reverse proxy requets
@ -141,11 +139,10 @@ type HeaderRewriteRules struct {
type AuthMethod int type AuthMethod int
const ( const (
AuthMethodNone AuthMethod = iota //No authentication required AuthMethodNone AuthMethod = iota //No authentication required
AuthMethodBasic //Basic Auth AuthMethodBasic //Basic Auth
AuthMethodAuthelia //Authelia AuthMethodForward //Forward
AuthMethodOauth2 //Oauth2 AuthMethodOauth2 //Oauth2
AuthMethodAuthentik
) )
type AuthenticationProvider struct { type AuthenticationProvider struct {
@ -155,9 +152,10 @@ type AuthenticationProvider struct {
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
BasicAuthGroupIDs []string //Group IDs that are allowed to access this endpoint BasicAuthGroupIDs []string //Group IDs that are allowed to access this endpoint
/* Authelia Settings */ /* Forward Auth Settings */
AutheliaURL string //URL of the Authelia server, leave empty to use global settings e.g. authelia.example.com ForwardAuthURL string // Full URL of the Forward Auth endpoint. Example: https://auth.example.com/api/authz/forward-auth
UseHTTPS bool //Whether to use HTTPS for the Authelia server ForwardAuthResponseHeaders []string // List of headers to copy from the forward auth server response to the request.
ForwardAuthRequestExcludedCookies []string // List of cookies to exclude from the request after sending it to the forward auth server.
} }
// A proxy endpoint record, a general interface for handling inbound routing // A proxy endpoint record, a general interface for handling inbound routing

View File

@ -59,7 +59,7 @@ type AuthProvider int
const ( const (
AuthProviderNone AuthProvider = iota AuthProviderNone AuthProvider = iota
AuthProviderBasicAuth AuthProviderBasicAuth
AuthProviderAuthelia AuthProviderForward
AuthProviderOauth2 AuthProviderOauth2
) )

View File

@ -115,8 +115,7 @@ func ReverseProxtInit() {
StatisticCollector: statisticCollector, StatisticCollector: statisticCollector,
WebDirectory: *path_webserver, WebDirectory: *path_webserver,
AccessController: accessController, AccessController: accessController,
AutheliaRouter: autheliaRouter, ForwardAuthRouter: forwardAuthRouter,
AuthentikRouter: authentikRouter,
LoadBalancer: loadBalancer, LoadBalancer: loadBalancer,
PluginManager: pluginManager, PluginManager: pluginManager,
/* Utilities */ /* Utilities */
@ -585,11 +584,9 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
if authProviderType == 1 { if authProviderType == 1 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodBasic newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodBasic
} else if authProviderType == 2 { } else if authProviderType == 2 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthelia newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodForward
} else if authProviderType == 3 { } else if authProviderType == 3 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2 newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2
} else if authProviderType == 4 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthentik
} else { } else {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodNone newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodNone
} }

View File

@ -15,7 +15,7 @@ import (
"imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/auth/sso/authelia" "imuslab.com/zoraxy/mod/auth/sso/forward"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/database/dbinc" "imuslab.com/zoraxy/mod/database/dbinc"
"imuslab.com/zoraxy/mod/dockerux" "imuslab.com/zoraxy/mod/dockerux"
@ -143,18 +143,10 @@ func startupSequence() {
} }
//Create authentication providers //Create authentication providers
autheliaRouter = authelia.NewAutheliaRouter(&authelia.AutheliaRouterOptions{ forwardAuthRouter = forward.NewAuthRouter(&forward.AuthRouterOptions{
UseHTTPS: false, // Automatic populate in router initiation Address: "",
AutheliaURL: "", // Automatic populate in router initiation Logger: SystemWideLogger,
Logger: SystemWideLogger, Database: sysdb,
Database: sysdb,
})
authentikRouter = authentik.NewAuthentikRouter(&authentik.AuthentikRouterOptions{
UseHTTPS: false, // Automatic populate in router initiation
AuthentikURL: "", // Automatic populate in router initiation
Logger: SystemWideLogger,
Database: sysdb,
}) })
//Create a statistic collector //Create a statistic collector

View File

@ -185,9 +185,8 @@
</td> </td>
<td data-label="" editable="true" datatype="advanced" style="width: 350px;"> <td data-label="" editable="true" datatype="advanced" style="width: 350px;">
${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``} ${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``}
${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Authelia`:``} ${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Forward Auth`:``}
${subd.AuthenticationProvider.AuthMethod == 0x3?`<i class="ui yellow key icon"></i> Oauth2`:``} ${subd.AuthenticationProvider.AuthMethod == 0x3?`<i class="ui yellow key icon"></i> OAuth2`:``}
${subd.AuthenticationProvider.AuthMethod == 0x4?`<i class="ui blue key icon"></i> Authentik`:``}
${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"<br>":""} ${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"<br>":""}
${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``} ${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``}
${subd.AuthenticationProvider.AuthMethod == 0x0 && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""} ${subd.AuthenticationProvider.AuthMethod == 0x0 && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""}
@ -393,13 +392,7 @@
<div class="field"> <div class="field">
<div class="ui radio checkbox"> <div class="ui radio checkbox">
<input type="radio" value="2" name="authProviderType" ${authProvider==0x2?"checked":""}> <input type="radio" value="2" name="authProviderType" ${authProvider==0x2?"checked":""}>
<label>Authelia</label> <label>Forward Auth</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="4" name="authProviderType" ${authProvider==0x4?"checked":""}>
<label>Authentik</label>
</div> </div>
</div> </div>
</div> </div>

View File

@ -14,44 +14,43 @@
</div> </div>
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="ui basic segment"> <div class="ui basic segment">
<h3>Authelia</h3> <h3>Forward Auth</h3>
<p>Configuration settings for Authelia authentication provider.</p> <p>Configuration settings for the Forward Auth provider.</p>
<p>The Forward Auth provider makes a subrequest to an authorization server that supports Forward Auth, then either:</p>
<ul>
<li>Allows the request to flow through to the backend when the authorization server responds with a 200-299 status code.</li>
<li>Responds with the response from the authorization server.</li>
</ul>
<p>Example authorization servers that support this:</p>
<ul>
<li><a href="https://www.authelia.com" rel=”noopener noreferrer target="_blank">Authelia</a></li>
<li><a href="https://goauthentik.io/" rel=”noopener noreferrer target="_blank">Authentik</a></li>
</ul>
<form class="ui form"> <form class="ui form">
<div class="field"> <div class="field">
<label for="autheliaServerUrl">Authelia Server URL</label> <label for="forwardAuthAddress">Address</label>
<input type="text" id="autheliaServerUrl" name="autheliaServerUrl" placeholder="Enter Authelia Server URL"> <input type="text" id="forwardAuthAddress" name="forwardAuthAddress" placeholder="Enter Forward Auth Address">
<small>Example: auth.example.com</small> <small>The full remote address or URL of the authorization servers forward auth endpoint. <strong>Example:</strong> https://auth.example.com/authz/forward-auth</small>
</div> </div>
<div class="field"> <div class="ui advancedSSOForwardAuthOptions accordion" style="margin-top:0.6em;">
<div class="ui checkbox"> <div class="title">
<input type="checkbox" id="useHttps" name="useHttps"> <i class="dropdown icon"></i>
<label for="useHttps">Use HTTPS</label> Advanced Options
<small>Check this if your authelia server uses HTTPS</small>
</div> </div>
</div> <div class="content">
<button class="ui basic button" onclick="event.preventDefault(); updateAutheliaSettings();"><i class="green check icon"></i> Apply Change</button> <div class="field">
</form> <label for="forwardAuthResponseHeaders">Response Headers</label>
</div> <input type="text" id="forwardAuthResponseHeaders" name="forwardAuthResponseHeaders" placeholder="Enter Forward Auth Response Headers">
<div class="ui divider"></div> <small>Comma separated list of case-insensitive headers to copy from the authorization servers response, to the request to the backend. <strong>Example:</strong> <code>Remote-User,Remote-Groups,Remote-Email,Remote-Name</code></small>
<div class="ui basic segment"> </div>
<h3>Authentik</h3> <div class="field">
<p>Configuration settings for Authentik authentication provider.</p> <label for="forwardAuthRequestExcludedCookies">Request Excluded Cookies</label>
<input type="text" id="forwardAuthRequestExcludedCookies" name="forwardAuthRequestExcludedCookies" placeholder="Enter Forward Auth Request Excluded Cookies">
<form class="ui form"> <small>Comma separated list of case-sensitive cookie names to exclude from the request to the backend. <strong>Example:</strong> <code>authelia_session,another_session</code></small>
<div class="field"> </div>
<label for="authentikServerUrl">Authentik Server URL</label>
<input type="text" id="authentikServerUrl" name="authentikServerUrl" placeholder="Enter Authentik Server URL">
<small>Example: auth.example.com</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="authentikUseHttps" name="useHttps">
<label for="authentikUseHttps">Use HTTPS</label>
<small>Check this if your Authentik server uses HTTPS</small>
</div> </div>
</div> </div><br />
<button class="ui basic button" onclick="event.preventDefault(); updateAuthentikSettings();"><i class="green check icon"></i> Apply Change</button> <button class="ui basic button" onclick="event.preventDefault(); updateForwardAuthSettings();"><i class="green check icon"></i> Apply Change</button>
</form> </form>
</div> </div>
<div class="ui divider"></div> <div class="ui divider"></div>
@ -60,24 +59,13 @@
<script> <script>
$(document).ready(function() { $(document).ready(function() {
$.cjax({ $.cjax({
url: '/api/sso/Authelia', url: '/api/sso/forward-auth',
method: 'GET', method: 'GET',
dataType: 'json', dataType: 'json',
success: function(data) { success: function(data) {
$('#autheliaServerUrl').val(data.autheliaURL); $('#forwardAuthAddress').val(data.address);
$('#useHttps').prop('checked', data.useHTTPS); $('#forwardAuthResponseHeaders').val(data.responseHeaders.join(","));
}, $('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(","));
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching SSO settings:', textStatus, errorThrown);
}
});
$.cjax({
url: '/api/sso/Authentik',
method: 'GET',
dataType: 'json',
success: function(data) {
$('#authentikServerUrl').val(data.authentikURL);
$('#authentikUseHttps').prop('checked', data.useHTTPS);
}, },
error: function(jqXHR, textStatus, errorThrown) { error: function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching SSO settings:', textStatus, errorThrown); console.error('Error fetching SSO settings:', textStatus, errorThrown);
@ -85,51 +73,31 @@
}); });
}); });
function updateAutheliaSettings(){ function updateForwardAuthSettings() {
var autheliaServerUrl = $('#autheliaServerUrl').val(); const address = $('#forwardAuthAddress').val();
var useHttps = $('#useHttps').prop('checked'); const responseHeaders = $('#forwardAuthResponseHeaders').val();
const requestExcludedCookies = $('#forwardAuthRequestExcludedCookies').val();
console.log(`Updating Forward Auth settings. Address: ${address}. Response Headers: ${responseHeaders}. Request Excluded Cookies: ${requestExcludedCookies}.`);
$.cjax({ $.cjax({
url: '/api/sso/Authelia', url: '/api/sso/forward-auth',
method: 'POST', method: 'POST',
data: { data: {
autheliaURL: autheliaServerUrl, address: address,
useHTTPS: useHttps responseHeaders: responseHeaders,
requestExcludedCookies: requestExcludedCookies
}, },
success: function(data) { success: function(data) {
if (data.error != undefined) { if (data.error !== undefined) {
$.msgbox(data.error, false); msgbox(data.error, false);
return; return;
} }
msgbox('Authelia settings updated', true); msgbox('Forward Auth settings updated', true);
console.log('Authelia settings updated:', data); console.log('Forward Auth settings updated:', data);
}, },
error: function(jqXHR, textStatus, errorThrown) { error: function(jqXHR, textStatus, errorThrown) {
console.error('Error updating Authelia settings:', textStatus, errorThrown); console.error('Error updating Forward Auth settings:', textStatus, errorThrown);
}
});
}
function updateAuthentikSettings(){
var authentikServerUrl = $('#authentikServerUrl').val();
var useHttps = $('#authentikUseHttps').prop('checked');
$.cjax({
url: '/api/sso/Authentik',
method: 'POST',
data: {
authentikURL: authentikServerUrl,
useHTTPS: useHttps
},
success: function(data) {
if (data.error != undefined) {
$.msgbox(data.error, false);
return;
}
msgbox('Authentik settings updated', true);
console.log('Authentik settings updated:', data);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error updating Authentik settings:', textStatus, errorThrown);
} }
}); });
} }