diff --git a/src/mod/auth/sso/forward/const.go b/src/mod/auth/sso/forward/const.go index a4e935d..6902e58 100644 --- a/src/mod/auth/sso/forward/const.go +++ b/src/mod/auth/sso/forward/const.go @@ -13,14 +13,21 @@ const ( DatabaseKeyRequestHeaders = "requestHeaders" DatabaseKeyRequestIncludedCookies = "requestIncludedCookies" DatabaseKeyRequestExcludedCookies = "requestExcludedCookies" + DatabaseKeyRequestIncludeBody = "requestIncludeBody" + DatabaseKeyUseXOriginalHeaders = "useXOriginalHeaders" HeaderXForwardedProto = "X-Forwarded-Proto" HeaderXForwardedHost = "X-Forwarded-Host" - HeaderXForwardedFor = "X-Forwarded-For" HeaderXForwardedURI = "X-Forwarded-URI" + HeaderXForwardedFor = "X-Forwarded-For" HeaderXForwardedMethod = "X-Forwarded-Method" - HeaderCookie = "Cookie" + HeaderXOriginalURL = "X-Original-URL" + HeaderXOriginalIP = "X-Original-IP" + HeaderXOriginalMethod = "X-Original-Method" + + HeaderCookie = "Cookie" + HeaderLocation = "Location" HeaderUpgrade = "Upgrade" HeaderConnection = "Connection" diff --git a/src/mod/auth/sso/forward/forward.go b/src/mod/auth/sso/forward/forward.go index 00ea450..5f1989f 100644 --- a/src/mod/auth/sso/forward/forward.go +++ b/src/mod/auth/sso/forward/forward.go @@ -2,8 +2,10 @@ package forward import ( "encoding/json" + "fmt" "io" "net/http" + "strconv" "strings" "imuslab.com/zoraxy/mod/database" @@ -34,6 +36,13 @@ type AuthRouterOptions struct { // RequestExcludedCookies is a list of cookie keys that should be removed from every request sent to the upstream. RequestExcludedCookies []string + // RequestIncludeBody enables copying the request body to the request to the authorization server. + RequestIncludeBody bool + + // UseXOriginalHeaders is a boolean that determines if the X-Original-* headers should be used instead of the + // X-Forwarded-* headers. + UseXOriginalHeaders bool + Logger *logger.Logger Database *database.Database } @@ -57,15 +66,8 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter { options.Database.Read(DatabaseTable, DatabaseKeyRequestHeaders, &requestHeaders) options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludedCookies, &requestIncludedCookies) options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies) - - // Helper function to clean empty strings from split results - cleanSplit := func(s string) []string { - if s == "" { - return nil - } - - return strings.Split(s, ",") - } + options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludeBody, &options.RequestIncludeBody) + options.Database.Read(DatabaseTable, DatabaseKeyUseXOriginalHeaders, &options.UseXOriginalHeaders) options.ResponseHeaders = cleanSplit(responseHeaders) options.ResponseClientHeaders = cleanSplit(responseClientHeaders) @@ -73,7 +75,7 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter { options.RequestIncludedCookies = cleanSplit(requestIncludedCookies) options.RequestExcludedCookies = cleanSplit(requestExcludedCookies) - return &AuthRouter{ + r := &AuthRouter{ client: &http.Client{ CheckRedirect: func(r *http.Request, via []*http.Request) (err error) { return http.ErrUseLastResponse @@ -81,6 +83,10 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter { }, options: options, } + + r.logOptions() + + return r } // HandleAPIOptions is the internal handler for setting the options. @@ -103,6 +109,8 @@ func (ar *AuthRouter) handleOptionsGET(w http.ResponseWriter, r *http.Request) { DatabaseKeyRequestHeaders: ar.options.RequestHeaders, DatabaseKeyRequestIncludedCookies: ar.options.RequestIncludedCookies, DatabaseKeyRequestExcludedCookies: ar.options.RequestExcludedCookies, + DatabaseKeyRequestIncludeBody: ar.options.RequestIncludeBody, + DatabaseKeyUseXOriginalHeaders: ar.options.UseXOriginalHeaders, }) utils.SendJSONResponse(w, string(js)) @@ -125,6 +133,8 @@ func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request) requestHeaders, _ := utils.PostPara(r, DatabaseKeyRequestHeaders) requestIncludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestIncludedCookies) requestExcludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestExcludedCookies) + requestIncludeBody, _ := utils.PostPara(r, DatabaseKeyRequestIncludeBody) + useXOriginalHeaders, _ := utils.PostPara(r, DatabaseKeyUseXOriginalHeaders) // Write changes to runtime ar.options.Address = address @@ -133,6 +143,8 @@ func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request) ar.options.RequestHeaders = strings.Split(requestHeaders, ",") ar.options.RequestIncludedCookies = strings.Split(requestIncludedCookies, ",") ar.options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",") + ar.options.RequestIncludeBody, _ = strconv.ParseBool(requestIncludeBody) + ar.options.UseXOriginalHeaders, _ = strconv.ParseBool(useXOriginalHeaders) // Write changes to database ar.options.Database.Write(DatabaseTable, DatabaseKeyAddress, address) @@ -141,6 +153,10 @@ func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request) ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestHeaders, requestHeaders) ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestIncludedCookies, requestIncludedCookies) ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestExcludedCookies, requestExcludedCookies) + ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestIncludeBody, ar.options.RequestIncludeBody) + ar.options.Database.Write(DatabaseTable, DatabaseKeyUseXOriginalHeaders, ar.options.UseXOriginalHeaders) + + ar.logOptions() utils.SendOK(w) } @@ -158,9 +174,6 @@ func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.R } // Make a request to Authz Server to verify the request - // TODO: Add opt-in support for copying the request body to the forward auth request. Currently it's just an - // empty body which is usually fine in most instances. It's likely best to see if anyone wants this feature - // as I'm unaware of any specific forward auth implementation that needs it. req, err := http.NewRequest(http.MethodGet, ar.options.Address, nil) if err != nil { return ar.handle500Error(w, err, "Unable to create request") @@ -171,7 +184,17 @@ func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.R // TODO: Add support for headers from upstream proxies. This will likely involve implementing some form of // proxy specific trust system within Zoraxy. - rSetForwardedHeaders(r, req) + if ar.options.UseXOriginalHeaders { + rSetXOriginalHeaders(r, req) + } else { + rSetXForwardedHeaders(r, req) + } + + if ar.options.RequestIncludeBody { + if err = rCopyBody(r, req); err != nil { + return ar.handle500Error(w, err, "Unable to perform forwarded auth due to a request copy error") + } + } // Make the Authz Request. respForwarded, err := ar.client.Do(req) @@ -202,15 +225,14 @@ func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.R // Copy the unsuccessful response. headerCopyExcluded(respForwarded.Header, w.Header(), nil) - w.WriteHeader(respForwarded.StatusCode) - - body, err := io.ReadAll(respForwarded.Body) - if err != nil { - return ar.handle500Error(w, err, "Unable to read response to forward auth request") + if ar.options.UseXOriginalHeaders && respForwarded.StatusCode == 401 && respForwarded.Header.Get(HeaderLocation) != "" { + w.WriteHeader(http.StatusFound) + } else { + w.WriteHeader(respForwarded.StatusCode) } - if _, err = w.Write(body); err != nil { - return ar.handle500Error(w, err, "Unable to write response") + if _, err = io.Copy(w, respForwarded.Body); err != nil { + return ar.handle500Error(w, err, "Unable to copy response") } return ErrUnauthorized @@ -224,3 +246,7 @@ func (ar *AuthRouter) handle500Error(w http.ResponseWriter, err error, message s return ErrInternalServerError } + +func (ar *AuthRouter) logOptions() { + ar.options.Logger.PrintAndLog(LogTitle, fmt.Sprintf("Forward Authz Options -> Address: %s, Response Headers: %s, Response Client Headers: %s, Request Headers: %s, Request Included Cookies: %s, Request Excluded Cookies: %s, Request Include Body: %t, Use X-Original Headers: %t", ar.options.Address, strings.Join(ar.options.ResponseHeaders, ";"), strings.Join(ar.options.ResponseClientHeaders, ";"), strings.Join(ar.options.RequestHeaders, ";"), strings.Join(ar.options.RequestIncludedCookies, ";"), strings.Join(ar.options.RequestExcludedCookies, ";"), ar.options.RequestIncludeBody, ar.options.UseXOriginalHeaders), nil) +} diff --git a/src/mod/auth/sso/forward/util.go b/src/mod/auth/sso/forward/util.go index 7ba9b3a..10a6af6 100644 --- a/src/mod/auth/sso/forward/util.go +++ b/src/mod/auth/sso/forward/util.go @@ -1,8 +1,11 @@ package forward import ( + "bytes" + "io" "net" "net/http" + "net/url" "strings" ) @@ -121,17 +124,65 @@ func stringInSliceFold(needle string, haystack []string) bool { 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()) - } +func rSetIPHeader(r, req *http.Request, headers ...string) { + if r.RemoteAddr == "" || len(headers) == 0 { + return } + before, _, _ := strings.Cut(r.RemoteAddr, ":") + + ip := net.ParseIP(before) + if ip == nil { + return + } + + for _, header := range headers { + req.Header.Set(header, ip.String()) + } +} + +func rSetXForwardedHeaders(r, req *http.Request) { + rSetIPHeader(r, req, HeaderXForwardedFor) 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) } + +func rSetXOriginalHeaders(r, req *http.Request) { + // The X-Forwarded-For header has larger support, so we include both. + rSetIPHeader(r, req, HeaderXOriginalIP, HeaderXForwardedFor) + + original := &url.URL{ + Scheme: scheme(r), + Host: r.Host, + Path: r.URL.Path, + } + + req.Header.Set(HeaderXOriginalMethod, r.Method) + req.Header.Set(HeaderXOriginalURL, original.String()) +} + +func rCopyBody(req, freq *http.Request) (err error) { + body, err := io.ReadAll(req.Body) + if err != nil { + return err + } + + if len(body) == 0 { + return nil + } + + req.Body = io.NopCloser(bytes.NewReader(body)) + freq.Body = io.NopCloser(bytes.NewReader(body)) + + return nil +} + +func cleanSplit(s string) []string { + if s == "" { + return nil + } + + return strings.Split(s, ",") +} diff --git a/src/web/components/sso.html b/src/web/components/sso.html index 0224f04..f8bf481 100644 --- a/src/web/components/sso.html +++ b/src/web/components/sso.html @@ -6,7 +6,7 @@
@@ -28,7 +28,7 @@
- The full remote address or URL of the authorization servers forward auth endpoint. Example: https://auth.example.com/authz/forward-auth + The full remote address or URL of the authorization servers forward auth endpoint. Example: http://127.0.0.1:9091/authz/forward-auth
@@ -78,6 +78,14 @@ Example: authelia_session,another_session
+
+ + +
+
+ + +
@@ -85,7 +93,7 @@
- +

OAuth 2.0

Configuration settings for OAuth 2.0 authentication provider.

@@ -96,7 +104,7 @@ Public identifier of the OAuth2 application
- + Secret key of the OAuth2 application
@@ -144,7 +152,7 @@ $(".ssoTabs .item").tab(); $(document).ready(function() { - /* Load forward-auth settings from backend */ + /* Load Forward Authz settings from backend */ $.cjax({ url: '/api/sso/forward-auth', method: 'GET', @@ -176,13 +184,23 @@ } else { $('#forwardAuthRequestExcludedCookies').val(""); } + if (data.requestIncludeBody != null && data.requestIncludeBody === true) { + $("#forwardAuthRequestIncludeBody").parent().checkbox("set checked"); + } else { + $("#forwardAuthRequestIncludeBody").parent().checkbox("set unchecked"); + } + if (data.useXOriginalHeaders != null && data.useXOriginalHeaders === true) { + $("#forwardAuthRequestUseXOriginalHeaders").parent().checkbox("set checked"); + } else { + $("#forwardAuthRequestUseXOriginalHeaders").parent().checkbox("set unchecked"); + } }, error: function(jqXHR, textStatus, errorThrown) { console.error('Error fetching SSO settings:', textStatus, errorThrown); } }); - /* Load Oauth2 settings from backend */ + /* Load OAuth 2.0 settings from backend */ $.cjax({ url: '/api/sso/OAuth2', method: 'GET', @@ -204,19 +222,22 @@ /* Add more initialization code here if needed */ }); - /* - Function to update Forward Auth settings. + /* + Forward Auth settings update handler. */ + $("#forwardAuthSettings").on("submit", function(event) { + event.preventDefault(); - function updateForwardAuthSettings() { const address = $('#forwardAuthAddress').val(); const responseHeaders = $('#forwardAuthResponseHeaders').val(); const responseClientHeaders = $('#forwardAuthResponseClientHeaders').val(); const requestHeaders = $('#forwardAuthRequestHeaders').val(); const requestIncludedCookies = $('#forwardAuthRequestIncludedCookies').val(); const requestExcludedCookies = $('#forwardAuthRequestExcludedCookies').val(); + const requestIncludeBody = $('#forwardAuthRequestIncludeBody').is(':checked'); + const useXOriginalHeaders = $('#forwardAuthRequestUseXOriginalHeaders').is(':checked'); - console.log(`Updating Forward Auth settings. Address: ${address}. Response Headers: ${responseHeaders}. Response Client Headers: ${responseClientHeaders}. Request Headers: ${requestHeaders}. Request Excluded Cookies: ${requestExcludedCookies}.`); + console.log(`Updating Forward Auth settings. Address: ${address}. Response Headers: ${responseHeaders}. Response Client Headers: ${responseClientHeaders}. Request Headers: ${requestHeaders}. Request Included Cookies: ${requestIncludedCookies}. Request Excluded Cookies: ${requestExcludedCookies}. Request Include Body: ${requestIncludeBody}. Use X-Original-* Headers: ${useXOriginalHeaders}.`); $.cjax({ url: '/api/sso/forward-auth', @@ -226,7 +247,10 @@ responseHeaders: responseHeaders, responseClientHeaders: responseClientHeaders, requestHeaders: requestHeaders, - requestExcludedCookies: requestExcludedCookies + requestIncludedCookies: requestIncludedCookies, + requestExcludedCookies: requestExcludedCookies, + requestIncludeBody: requestIncludeBody, + useXOriginalHeaders: useXOriginalHeaders, }, success: function(data) { if (data.error !== undefined) { @@ -240,42 +264,11 @@ console.error('Error updating Forward Auth settings:', textStatus, errorThrown); } }); - } - - $("#forwardAuthSettings").on("submit", function(event) { - event.preventDefault(); - updateForwardAuthSettings(); }); /* - Oauth2 settings update handler. + OAuth 2.0 settings update handler. */ - $( "#authentikSettings" ).on( "submit", function( event ) { - event.preventDefault(); - $.cjax({ - url: '/api/sso/forward-auth', - method: 'POST', - data: { - address: address, - responseHeaders: responseHeaders, - responseClientHeaders: responseClientHeaders, - requestHeaders: requestHeaders, - requestExcludedCookies: requestExcludedCookies - }, - success: function(data) { - if (data.error !== undefined) { - msgbox(data.error, false); - return; - } - msgbox('Forward Auth settings updated', true); - console.log('Forward Auth settings updated:', data); - }, - error: function(jqXHR, textStatus, errorThrown) { - console.error('Error updating Forward Auth settings:', textStatus, errorThrown); - } - }); - }); - $( "#oauth2Settings" ).on( "submit", function( event ) { event.preventDefault(); $.cjax({