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 @@
+
Configuration settings for Authentik authentication provider.
+ + +