mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-09-15 16:49:41 +02:00
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:
@@ -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"
|
||||
|
||||
HeaderXOriginalURL = "X-Original-URL"
|
||||
HeaderXOriginalIP = "X-Original-IP"
|
||||
HeaderXOriginalMethod = "X-Original-Method"
|
||||
|
||||
HeaderCookie = "Cookie"
|
||||
HeaderLocation = "Location"
|
||||
|
||||
HeaderUpgrade = "Upgrade"
|
||||
HeaderConnection = "Connection"
|
||||
|
@@ -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)
|
||||
|
||||
if ar.options.UseXOriginalHeaders && respForwarded.StatusCode == 401 && respForwarded.Header.Get(HeaderLocation) != "" {
|
||||
w.WriteHeader(http.StatusFound)
|
||||
} else {
|
||||
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 _, 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)
|
||||
}
|
||||
|
@@ -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 != "" {
|
||||
func rSetIPHeader(r, req *http.Request, headers ...string) {
|
||||
if r.RemoteAddr == "" || len(headers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
before, _, _ := strings.Cut(r.RemoteAddr, ":")
|
||||
|
||||
if ip := net.ParseIP(before); ip != nil {
|
||||
req.Header.Set(HeaderXForwardedFor, ip.String())
|
||||
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, ",")
|
||||
}
|
||||
|
@@ -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',
|
||||
@@ -205,18 +223,21 @@
|
||||
});
|
||||
|
||||
/*
|
||||
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({
|
||||
|
Reference in New Issue
Block a user