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" DatabaseKeyRequestHeaders = "requestHeaders"
DatabaseKeyRequestIncludedCookies = "requestIncludedCookies" DatabaseKeyRequestIncludedCookies = "requestIncludedCookies"
DatabaseKeyRequestExcludedCookies = "requestExcludedCookies" DatabaseKeyRequestExcludedCookies = "requestExcludedCookies"
DatabaseKeyRequestIncludeBody = "requestIncludeBody"
DatabaseKeyUseXOriginalHeaders = "useXOriginalHeaders"
HeaderXForwardedProto = "X-Forwarded-Proto" HeaderXForwardedProto = "X-Forwarded-Proto"
HeaderXForwardedHost = "X-Forwarded-Host" HeaderXForwardedHost = "X-Forwarded-Host"
HeaderXForwardedFor = "X-Forwarded-For"
HeaderXForwardedURI = "X-Forwarded-URI" HeaderXForwardedURI = "X-Forwarded-URI"
HeaderXForwardedFor = "X-Forwarded-For"
HeaderXForwardedMethod = "X-Forwarded-Method" HeaderXForwardedMethod = "X-Forwarded-Method"
HeaderXOriginalURL = "X-Original-URL"
HeaderXOriginalIP = "X-Original-IP"
HeaderXOriginalMethod = "X-Original-Method"
HeaderCookie = "Cookie" HeaderCookie = "Cookie"
HeaderLocation = "Location"
HeaderUpgrade = "Upgrade" HeaderUpgrade = "Upgrade"
HeaderConnection = "Connection" HeaderConnection = "Connection"

View File

@@ -2,8 +2,10 @@ package forward
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"strconv"
"strings" "strings"
"imuslab.com/zoraxy/mod/database" "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 is a list of cookie keys that should be removed from every request sent to the upstream.
RequestExcludedCookies []string 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 Logger *logger.Logger
Database *database.Database Database *database.Database
} }
@@ -57,15 +66,8 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter {
options.Database.Read(DatabaseTable, DatabaseKeyRequestHeaders, &requestHeaders) options.Database.Read(DatabaseTable, DatabaseKeyRequestHeaders, &requestHeaders)
options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludedCookies, &requestIncludedCookies) options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludedCookies, &requestIncludedCookies)
options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies) options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies)
options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludeBody, &options.RequestIncludeBody)
// Helper function to clean empty strings from split results options.Database.Read(DatabaseTable, DatabaseKeyUseXOriginalHeaders, &options.UseXOriginalHeaders)
cleanSplit := func(s string) []string {
if s == "" {
return nil
}
return strings.Split(s, ",")
}
options.ResponseHeaders = cleanSplit(responseHeaders) options.ResponseHeaders = cleanSplit(responseHeaders)
options.ResponseClientHeaders = cleanSplit(responseClientHeaders) options.ResponseClientHeaders = cleanSplit(responseClientHeaders)
@@ -73,7 +75,7 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter {
options.RequestIncludedCookies = cleanSplit(requestIncludedCookies) options.RequestIncludedCookies = cleanSplit(requestIncludedCookies)
options.RequestExcludedCookies = cleanSplit(requestExcludedCookies) options.RequestExcludedCookies = cleanSplit(requestExcludedCookies)
return &AuthRouter{ r := &AuthRouter{
client: &http.Client{ client: &http.Client{
CheckRedirect: func(r *http.Request, via []*http.Request) (err error) { CheckRedirect: func(r *http.Request, via []*http.Request) (err error) {
return http.ErrUseLastResponse return http.ErrUseLastResponse
@@ -81,6 +83,10 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter {
}, },
options: options, options: options,
} }
r.logOptions()
return r
} }
// HandleAPIOptions is the internal handler for setting the options. // 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, DatabaseKeyRequestHeaders: ar.options.RequestHeaders,
DatabaseKeyRequestIncludedCookies: ar.options.RequestIncludedCookies, DatabaseKeyRequestIncludedCookies: ar.options.RequestIncludedCookies,
DatabaseKeyRequestExcludedCookies: ar.options.RequestExcludedCookies, DatabaseKeyRequestExcludedCookies: ar.options.RequestExcludedCookies,
DatabaseKeyRequestIncludeBody: ar.options.RequestIncludeBody,
DatabaseKeyUseXOriginalHeaders: ar.options.UseXOriginalHeaders,
}) })
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
@@ -125,6 +133,8 @@ func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request)
requestHeaders, _ := utils.PostPara(r, DatabaseKeyRequestHeaders) requestHeaders, _ := utils.PostPara(r, DatabaseKeyRequestHeaders)
requestIncludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestIncludedCookies) requestIncludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestIncludedCookies)
requestExcludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestExcludedCookies) requestExcludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestExcludedCookies)
requestIncludeBody, _ := utils.PostPara(r, DatabaseKeyRequestIncludeBody)
useXOriginalHeaders, _ := utils.PostPara(r, DatabaseKeyUseXOriginalHeaders)
// Write changes to runtime // Write changes to runtime
ar.options.Address = address 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.RequestHeaders = strings.Split(requestHeaders, ",")
ar.options.RequestIncludedCookies = strings.Split(requestIncludedCookies, ",") ar.options.RequestIncludedCookies = strings.Split(requestIncludedCookies, ",")
ar.options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",") ar.options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
ar.options.RequestIncludeBody, _ = strconv.ParseBool(requestIncludeBody)
ar.options.UseXOriginalHeaders, _ = strconv.ParseBool(useXOriginalHeaders)
// Write changes to database // Write changes to database
ar.options.Database.Write(DatabaseTable, DatabaseKeyAddress, address) 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, DatabaseKeyRequestHeaders, requestHeaders)
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestIncludedCookies, requestIncludedCookies) ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestIncludedCookies, requestIncludedCookies)
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestExcludedCookies, requestExcludedCookies) 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) 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 // 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) req, err := http.NewRequest(http.MethodGet, ar.options.Address, nil)
if err != nil { if err != nil {
return ar.handle500Error(w, err, "Unable to create request") 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 // TODO: Add support for headers from upstream proxies. This will likely involve implementing some form of
// proxy specific trust system within Zoraxy. // 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. // Make the Authz Request.
respForwarded, err := ar.client.Do(req) respForwarded, err := ar.client.Do(req)
@@ -202,15 +225,14 @@ func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.R
// Copy the unsuccessful response. // Copy the unsuccessful response.
headerCopyExcluded(respForwarded.Header, w.Header(), nil) 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) 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 { if _, err = io.Copy(w, respForwarded.Body); err != nil {
return ar.handle500Error(w, err, "Unable to write response") return ar.handle500Error(w, err, "Unable to copy response")
} }
return ErrUnauthorized return ErrUnauthorized
@@ -224,3 +246,7 @@ func (ar *AuthRouter) handle500Error(w http.ResponseWriter, err error, message s
return ErrInternalServerError 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 package forward
import ( import (
"bytes"
"io"
"net" "net"
"net/http" "net/http"
"net/url"
"strings" "strings"
) )
@@ -121,17 +124,65 @@ func stringInSliceFold(needle string, haystack []string) bool {
return false return false
} }
func rSetForwardedHeaders(r, req *http.Request) { func rSetIPHeader(r, req *http.Request, headers ...string) {
if r.RemoteAddr != "" { if r.RemoteAddr == "" || len(headers) == 0 {
return
}
before, _, _ := strings.Cut(r.RemoteAddr, ":") before, _, _ := strings.Cut(r.RemoteAddr, ":")
if ip := net.ParseIP(before); ip != nil { ip := net.ParseIP(before)
req.Header.Set(HeaderXForwardedFor, ip.String()) 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(HeaderXForwardedMethod, r.Method)
req.Header.Set(HeaderXForwardedProto, scheme(r)) req.Header.Set(HeaderXForwardedProto, scheme(r))
req.Header.Set(HeaderXForwardedHost, r.Host) req.Header.Set(HeaderXForwardedHost, r.Host)
req.Header.Set(HeaderXForwardedURI, r.URL.Path) 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 divider"></div>
<div class="ui top attached tabular menu ssoTabs"> <div class="ui top attached tabular menu ssoTabs">
<a class="item active" data-tab="forward_auth_tab">Forward Auth</a> <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> --> <!-- <a class="item" data-tab="zoraxy_sso_tab">Zoraxy SSO</a> -->
</div> </div>
<div class="ui bottom attached tab segment active" data-tab="forward_auth_tab"> <div class="ui bottom attached tab segment active" data-tab="forward_auth_tab">
@@ -28,7 +28,7 @@
<div class="field"> <div class="field">
<label for="forwardAuthAddress">Address</label> <label for="forwardAuthAddress">Address</label>
<input type="text" id="forwardAuthAddress" name="forwardAuthAddress" placeholder="Enter Forward Auth Address"> <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>
<div class="ui basic segment advanceoptions" style="margin-top:0.6em;"> <div class="ui basic segment advanceoptions" style="margin-top:0.6em;">
<div class="ui advancedSSOForwardAuthOptions accordion"> <div class="ui advancedSSOForwardAuthOptions accordion">
@@ -78,6 +78,14 @@
<strong>Example:</strong> <code>authelia_session,another_session</code> <strong>Example:</strong> <code>authelia_session,another_session</code>
</small> </small>
</div> </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> </div>
</div> </div>
@@ -85,7 +93,7 @@
</form> </form>
</div> </div>
<div class="ui bottom attached tab segment" data-tab="oauth2_tab"> <div class="ui bottom attached tab segment" data-tab="oauth2_tab">
<!-- Oauth 2 --> <!-- OAuth 2.0 -->
<h2>OAuth 2.0</h2> <h2>OAuth 2.0</h2>
<p>Configuration settings for OAuth 2.0 authentication provider.</p> <p>Configuration settings for OAuth 2.0 authentication provider.</p>
@@ -96,7 +104,7 @@
<small>Public identifier of the OAuth2 application</small> <small>Public identifier of the OAuth2 application</small>
</div> </div>
<div class="field"> <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"> <input type="password" id="oauth2ClientSecret" name="oauth2ClientSecret" placeholder="Enter Client Secret">
<small>Secret key of the OAuth2 application</small> <small>Secret key of the OAuth2 application</small>
</div> </div>
@@ -144,7 +152,7 @@
$(".ssoTabs .item").tab(); $(".ssoTabs .item").tab();
$(document).ready(function() { $(document).ready(function() {
/* Load forward-auth settings from backend */ /* Load Forward Authz settings from backend */
$.cjax({ $.cjax({
url: '/api/sso/forward-auth', url: '/api/sso/forward-auth',
method: 'GET', method: 'GET',
@@ -176,13 +184,23 @@
} else { } else {
$('#forwardAuthRequestExcludedCookies').val(""); $('#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) { error: function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching SSO settings:', textStatus, errorThrown); console.error('Error fetching SSO settings:', textStatus, errorThrown);
} }
}); });
/* Load Oauth2 settings from backend */ /* Load OAuth 2.0 settings from backend */
$.cjax({ $.cjax({
url: '/api/sso/OAuth2', url: '/api/sso/OAuth2',
method: 'GET', 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 address = $('#forwardAuthAddress').val();
const responseHeaders = $('#forwardAuthResponseHeaders').val(); const responseHeaders = $('#forwardAuthResponseHeaders').val();
const responseClientHeaders = $('#forwardAuthResponseClientHeaders').val(); const responseClientHeaders = $('#forwardAuthResponseClientHeaders').val();
const requestHeaders = $('#forwardAuthRequestHeaders').val(); const requestHeaders = $('#forwardAuthRequestHeaders').val();
const requestIncludedCookies = $('#forwardAuthRequestIncludedCookies').val(); const requestIncludedCookies = $('#forwardAuthRequestIncludedCookies').val();
const requestExcludedCookies = $('#forwardAuthRequestExcludedCookies').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({ $.cjax({
url: '/api/sso/forward-auth', url: '/api/sso/forward-auth',
@@ -226,7 +247,10 @@
responseHeaders: responseHeaders, responseHeaders: responseHeaders,
responseClientHeaders: responseClientHeaders, responseClientHeaders: responseClientHeaders,
requestHeaders: requestHeaders, requestHeaders: requestHeaders,
requestExcludedCookies: requestExcludedCookies requestIncludedCookies: requestIncludedCookies,
requestExcludedCookies: requestExcludedCookies,
requestIncludeBody: requestIncludeBody,
useXOriginalHeaders: useXOriginalHeaders,
}, },
success: function(data) { success: function(data) {
if (data.error !== undefined) { if (data.error !== undefined) {
@@ -240,42 +264,11 @@
console.error('Error updating Forward Auth settings:', textStatus, errorThrown); 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 ) { $( "#oauth2Settings" ).on( "submit", function( event ) {
event.preventDefault(); event.preventDefault();
$.cjax({ $.cjax({