Merge pull request #788 from james-d-elliott/feat-forward-auith-original

feat(sso): forward auth body and alternate headers
This commit is contained in:
Toby Chui
2025-09-06 12:51:14 +08:00
committed by GitHub
4 changed files with 150 additions and 73 deletions

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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, ",")
}

View File

@@ -6,7 +6,7 @@
<div class="ui divider"></div>
<div class="ui top attached tabular menu ssoTabs">
<a class="item active" data-tab="forward_auth_tab">Forward Auth</a>
<a class="item" data-tab="oauth2_tab">Oauth2</a>
<a class="item" data-tab="oauth2_tab">OAuth 2.0</a>
<!-- <a class="item" data-tab="zoraxy_sso_tab">Zoraxy SSO</a> -->
</div>
<div class="ui bottom attached tab segment active" data-tab="forward_auth_tab">
@@ -28,7 +28,7 @@
<div class="field">
<label for="forwardAuthAddress">Address</label>
<input type="text" id="forwardAuthAddress" name="forwardAuthAddress" placeholder="Enter Forward Auth Address">
<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>
<small>The full remote address or URL of the authorization servers forward auth endpoint. <strong>Example:</strong> http://127.0.0.1:9091/authz/forward-auth</small>
</div>
<div class="ui basic segment advanceoptions" style="margin-top:0.6em;">
<div class="ui advancedSSOForwardAuthOptions accordion">
@@ -78,6 +78,14 @@
<strong>Example:</strong> <code>authelia_session,another_session</code>
</small>
</div>
<div class="ui checkbox">
<input type="checkbox" id="forwardAuthRequestIncludeBody" name="forwardAuthRequestIncludeBody" value="Forward Auth Request Include Request Body">
<label for="forwardAuthRequestIncludeBody">Forward Auth Request Include Request Body<br><small>This allows the request body from the <b><i>request made from the client</i></b> to be included in the <b><i>request made to the authorization server</i></b>. Generally this should not be enabled.</small></label>
</div>
<div class="ui checkbox">
<input type="checkbox" id="forwardAuthRequestUseXOriginalHeaders" name="forwardAuthRequestUseXOriginalHeaders" value="Use X-Original-* Headers">
<label for="forwardAuthRequestUseXOriginalHeaders">Use X-Original-* Headers<br><small>This is used for implementations which do not use the X-Forwarded-* headers. In addition if the authorization server responds with a 401 and Location header the status will be changed to 302.</small></label>
</div>
</div>
</div>
</div>
@@ -85,7 +93,7 @@
</form>
</div>
<div class="ui bottom attached tab segment" data-tab="oauth2_tab">
<!-- Oauth 2 -->
<!-- OAuth 2.0 -->
<h2>OAuth 2.0</h2>
<p>Configuration settings for OAuth 2.0 authentication provider.</p>
@@ -96,7 +104,7 @@
<small>Public identifier of the OAuth2 application</small>
</div>
<div class="field">
<label for="oauth2ClientId">Client Secret</label>
<label for="oauth2ClientSecret">Client Secret</label>
<input type="password" id="oauth2ClientSecret" name="oauth2ClientSecret" placeholder="Enter Client Secret">
<small>Secret key of the OAuth2 application</small>
</div>
@@ -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({