From ebf6ad6600743582f176830f086ccc351f67017e Mon Sep 17 00:00:00 2001 From: Joker <1465267+JokerQyou@users.noreply.github.com> Date: Sat, 1 Mar 2025 15:29:36 +0800 Subject: [PATCH 1/2] Add Authentik forward auth support --- src/api.go | 1 + src/def.go | 4 +- src/mod/auth/sso/authentik/authentik.go | 169 ++++++++++++++++++++++++ src/mod/dynamicproxy/authProviders.go | 10 ++ src/mod/dynamicproxy/typedef.go | 5 +- src/reverseproxy.go | 3 + src/start.go | 8 ++ src/web/components/httprp.html | 7 + src/web/components/sso.html | 57 ++++++++ 9 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 src/mod/auth/sso/authentik/authentik.go diff --git a/src/api.go b/src/api.go index 1507289..889ef16 100644 --- a/src/api.go +++ b/src/api.go @@ -81,6 +81,7 @@ func RegisterTLSAPIs(authRouter *auth.RouterDef) { // Register the APIs for Authentication handlers like Authelia and OAUTH2 func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) { authRouter.HandleFunc("/api/sso/Authelia", autheliaRouter.HandleSetAutheliaURLAndHTTPS) + authRouter.HandleFunc("/api/sso/Authentik", authentikRouter.HandleSetAuthentikURLAndHTTPS) } // Register the APIs for redirection rules management functions diff --git a/src/def.go b/src/def.go index 23d2ae4..f6d1aeb 100644 --- a/src/def.go +++ b/src/def.go @@ -10,6 +10,7 @@ package main import ( "embed" "flag" + "imuslab.com/zoraxy/mod/auth/sso/authentik" "net/http" "time" @@ -141,7 +142,8 @@ var ( loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing //Authentication Provider - autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication + autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication + authentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication //Helper modules EmailSender *email.Sender //Email sender that handle email sending diff --git a/src/mod/auth/sso/authentik/authentik.go b/src/mod/auth/sso/authentik/authentik.go new file mode 100644 index 0000000..8c621e2 --- /dev/null +++ b/src/mod/auth/sso/authentik/authentik.go @@ -0,0 +1,169 @@ +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.URL.Path, 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) + 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.Add("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 +} diff --git a/src/mod/dynamicproxy/authProviders.go b/src/mod/dynamicproxy/authProviders.go index f50cd55..d7e8190 100644 --- a/src/mod/dynamicproxy/authProviders.go +++ b/src/mod/dynamicproxy/authProviders.go @@ -43,6 +43,12 @@ func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *htt h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401) return true } + } else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthentik { + err := h.handleAuthentikAuth(w, r) + if err != nil { + h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401) + return true + } } //No authentication provider, do not need to handle @@ -106,3 +112,7 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) func (h *ProxyHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Request) error { return h.Parent.Option.AutheliaRouter.HandleAutheliaAuth(w, r) } + +func (h *ProxyHandler) handleAuthentikAuth(w http.ResponseWriter, r *http.Request) error { + return h.Parent.Option.AuthentikRouter.HandleAuthentikAuth(w, r) +} diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index 6761674..8a65398 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -9,6 +9,7 @@ package dynamicproxy */ import ( _ "embed" + "imuslab.com/zoraxy/mod/auth/sso/authentik" "net" "net/http" "sync" @@ -61,7 +62,8 @@ type RouterOption struct { LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target /* Authentication Providers */ - AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication + AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication + AuthentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication /* Utilities */ Logger *logger.Logger //Logger for reverse proxy requets @@ -141,6 +143,7 @@ const ( AuthMethodBasic //Basic Auth AuthMethodAuthelia //Authelia AuthMethodOauth2 //Oauth2 + AuthMethodAuthentik ) type AuthenticationProvider struct { diff --git a/src/reverseproxy.go b/src/reverseproxy.go index e25e405..bba9515 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -115,6 +115,7 @@ func ReverseProxtInit() { WebDirectory: *path_webserver, AccessController: accessController, AutheliaRouter: autheliaRouter, + AuthentikRouter: authentikRouter, LoadBalancer: loadBalancer, Logger: SystemWideLogger, }) @@ -578,6 +579,8 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) { newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthelia } else if authProviderType == 3 { newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2 + } else if authProviderType == 4 { + newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthentik } else { newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodNone } diff --git a/src/start.go b/src/start.go index d237b8a..425361a 100644 --- a/src/start.go +++ b/src/start.go @@ -1,6 +1,7 @@ package main import ( + "imuslab.com/zoraxy/mod/auth/sso/authentik" "log" "net/http" "os" @@ -146,6 +147,13 @@ func startupSequence() { 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 statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{ Database: sysdb, diff --git a/src/web/components/httprp.html b/src/web/components/httprp.html index 871d0eb..ae8e65e 100644 --- a/src/web/components/httprp.html +++ b/src/web/components/httprp.html @@ -174,6 +174,7 @@ ${subd.AuthenticationProvider.AuthMethod == 0x1?` Basic Auth`:``} ${subd.AuthenticationProvider.AuthMethod == 0x2?` Authelia`:``} ${subd.AuthenticationProvider.AuthMethod == 0x3?` Oauth2`:``} + ${subd.AuthenticationProvider.AuthMethod == 0x4?` Authentik`:``} ${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"
":""} ${subd.RequireRateLimit?` Rate Limit @ ${subd.RateLimit} req/s`:``} ${subd.AuthenticationProvider.AuthMethod == 0x0 && !subd.RequireRateLimit?`No Special Settings`:""} @@ -382,6 +383,12 @@ +
+
+ + +
+
diff --git a/src/web/components/sso.html b/src/web/components/sso.html index af1639a..820735a 100644 --- a/src/web/components/sso.html +++ b/src/web/components/sso.html @@ -34,6 +34,27 @@
+
+

Authentik

+

Configuration settings for Authentik authentication provider.

+ +
+
+ + + Example: auth.example.com +
+
+
+ + + Check this if your Authentik server uses HTTPS +
+
+ +
+
+
\ No newline at end of file From 6a8057c3a703948871698a91b32c7dae95c92b77 Mon Sep 17 00:00:00 2001 From: Joker <1465267+JokerQyou@users.noreply.github.com> Date: Sun, 2 Mar 2025 00:04:08 +0800 Subject: [PATCH 2/2] fix passing wrong URI to Authentik outpost --- src/mod/auth/sso/authentik/authentik.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mod/auth/sso/authentik/authentik.go b/src/mod/auth/sso/authentik/authentik.go index 8c621e2..795b0b3 100644 --- a/src/mod/auth/sso/authentik/authentik.go +++ b/src/mod/auth/sso/authentik/authentik.go @@ -106,7 +106,7 @@ func (ar *AuthentikRouter) HandleAuthentikAuth(w http.ResponseWriter, r *http.Re 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.URL.Path, nil) + 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) @@ -144,7 +144,7 @@ func (ar *AuthentikRouter) HandleAuthentikAuth(w http.ResponseWriter, r *http.Re return errors.New("unauthorized") } - req.Header.Add("X-Original-URL", reqUrl) + req.Header.Set("X-Original-URL", reqUrl) // Copy cookies from the incoming request for _, cookie := range r.Cookies() {