From 7efc7da9abb1ed23695c33bc13190aa18b822fd0 Mon Sep 17 00:00:00 2001 From: kjagosz <63209438+kjagosz@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:04:28 +0200 Subject: [PATCH] - Introduce configurable OAuth2 configuration cache time via UI and backend - Refactor OAuth2Router to use `OAuth2ConfigurationCacheTime` with a default of 60s --- src/def.go | 1 - src/mod/auth/sso/oauth2/oauth2.go | 69 ++++++++++++++++++++----------- src/mod/utils/utils.go | 20 +++++++++ src/start.go | 5 +-- src/web/components/sso.html | 8 ++++ 5 files changed, 76 insertions(+), 27 deletions(-) diff --git a/src/def.go b/src/def.go index a592568..01a5909 100644 --- a/src/def.go +++ b/src/def.go @@ -93,7 +93,6 @@ var ( enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)") allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder") enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected") - oauth2ConfigurationCache = flag.Duration("oauth2cc", 60*time.Second, "Time in seconds to cache OAuth2 configuration, set to 0 to disable cache. Default: 60 seconds") /* Logging Configuration Flags */ enableLog = flag.Bool("enablelog", true, "Enable system wide logging, set to false for writing log to STDOUT only") diff --git a/src/mod/auth/sso/oauth2/oauth2.go b/src/mod/auth/sso/oauth2/oauth2.go index 1dbde61..4108ce2 100644 --- a/src/mod/auth/sso/oauth2/oauth2.go +++ b/src/mod/auth/sso/oauth2/oauth2.go @@ -16,19 +16,24 @@ import ( "imuslab.com/zoraxy/mod/utils" ) +const ( + // DefaultOAuth2ConfigCacheTime defines the default cache duration for OAuth2 configuration + DefaultOAuth2ConfigCacheTime = 60 * time.Second +) + type OAuth2RouterOptions struct { - OAuth2ServerURL string //The URL of the OAuth 2.0 server server - OAuth2TokenURL string //The URL of the OAuth 2.0 token server - OAuth2ClientId string //The client id for OAuth 2.0 Application - OAuth2ClientSecret string //The client secret for OAuth 2.0 Application - OAuth2WellKnownUrl string //The well-known url for OAuth 2.0 server - OAuth2UserInfoUrl string //The URL of the OAuth 2.0 user info endpoint - OAuth2Scopes string //The scopes for OAuth 2.0 Application - OAuth2CodeChallengeMethod string //The authorization code challenge method - Logger *logger.Logger - Database *database.Database - OAuth2ConfigCacheTTL *time.Duration - OAuth2ConfigCache *ttlcache.Cache[string, *oauth2.Config] + OAuth2ServerURL string //The URL of the OAuth 2.0 server server + OAuth2TokenURL string //The URL of the OAuth 2.0 token server + OAuth2ClientId string //The client id for OAuth 2.0 Application + OAuth2ClientSecret string //The client secret for OAuth 2.0 Application + OAuth2WellKnownUrl string //The well-known url for OAuth 2.0 server + OAuth2UserInfoUrl string //The URL of the OAuth 2.0 user info endpoint + OAuth2Scopes string //The scopes for OAuth 2.0 Application + OAuth2CodeChallengeMethod string //The authorization code challenge method + OAuth2ConfigurationCacheTime *time.Duration + Logger *logger.Logger + Database *database.Database + OAuth2ConfigCache *ttlcache.Cache[string, *oauth2.Config] } type OIDCDiscoveryDocument struct { @@ -64,13 +69,20 @@ func NewOAuth2Router(options *OAuth2RouterOptions) *OAuth2Router { options.Database.Read("oauth2", "oauth2UserInfoUrl", &options.OAuth2UserInfoUrl) options.Database.Read("oauth2", "oauth2CodeChallengeMethod", &options.OAuth2CodeChallengeMethod) options.Database.Read("oauth2", "oauth2Scopes", &options.OAuth2Scopes) + options.Database.Read("oauth2", "oauth2ConfigurationCacheTime", &options.OAuth2ConfigurationCacheTime) ar := &OAuth2Router{ options: options, } + if options.OAuth2ConfigurationCacheTime == nil || + options.OAuth2ConfigurationCacheTime.Seconds() == 0 { + cacheTime := DefaultOAuth2ConfigCacheTime + options.OAuth2ConfigurationCacheTime = &cacheTime + } + options.OAuth2ConfigCache = ttlcache.New[string, *oauth2.Config]( - ttlcache.WithTTL[string, *oauth2.Config](*options.OAuth2ConfigCacheTTL), + ttlcache.WithTTL[string, *oauth2.Config](*options.OAuth2ConfigurationCacheTime), ) go options.OAuth2ConfigCache.Start() @@ -94,14 +106,15 @@ func (ar *OAuth2Router) HandleSetOAuth2Settings(w http.ResponseWriter, r *http.R func (ar *OAuth2Router) handleSetOAuthSettingsGET(w http.ResponseWriter, r *http.Request) { //Return the current settings js, _ := json.Marshal(map[string]interface{}{ - "oauth2WellKnownUrl": ar.options.OAuth2WellKnownUrl, - "oauth2ServerUrl": ar.options.OAuth2ServerURL, - "oauth2TokenUrl": ar.options.OAuth2TokenURL, - "oauth2UserInfoUrl": ar.options.OAuth2UserInfoUrl, - "oauth2Scopes": ar.options.OAuth2Scopes, - "oauth2ClientSecret": ar.options.OAuth2ClientSecret, - "oauth2ClientId": ar.options.OAuth2ClientId, - "oauth2CodeChallengeMethod": ar.options.OAuth2CodeChallengeMethod, + "oauth2WellKnownUrl": ar.options.OAuth2WellKnownUrl, + "oauth2ServerUrl": ar.options.OAuth2ServerURL, + "oauth2TokenUrl": ar.options.OAuth2TokenURL, + "oauth2UserInfoUrl": ar.options.OAuth2UserInfoUrl, + "oauth2Scopes": ar.options.OAuth2Scopes, + "oauth2ClientSecret": ar.options.OAuth2ClientSecret, + "oauth2ClientId": ar.options.OAuth2ClientId, + "oauth2CodeChallengeMethod": ar.options.OAuth2CodeChallengeMethod, + "oauth2ConfigurationCacheTime": ar.options.OAuth2ConfigurationCacheTime.String(), }) utils.SendJSONResponse(w, string(js)) @@ -110,6 +123,7 @@ func (ar *OAuth2Router) handleSetOAuthSettingsGET(w http.ResponseWriter, r *http func (ar *OAuth2Router) handleSetOAuthSettingsPOST(w http.ResponseWriter, r *http.Request) { //Update the settings var oauth2ServerUrl, oauth2TokenURL, oauth2Scopes, oauth2UserInfoUrl, oauth2CodeChallengeMethod string + var oauth2ConfigurationCacheTime *time.Duration oauth2ClientId, err := utils.PostPara(r, "oauth2ClientId") if err != nil { @@ -129,8 +143,14 @@ func (ar *OAuth2Router) handleSetOAuthSettingsPOST(w http.ResponseWriter, r *htt return } - oauth2WellKnownUrl, err := utils.PostPara(r, "oauth2WellKnownUrl") + oauth2ConfigurationCacheTime, err = utils.PostDuration(r, "oauth2ConfigurationCacheTime") if err != nil { + utils.SendErrorResponse(w, "oauth2ConfigurationCacheTime not found") + return + } + + oauth2WellKnownUrl, err := utils.PostPara(r, "oauth2WellKnownUrl") + if err != nil || oauth2WellKnownUrl == "" { oauth2ServerUrl, err = utils.PostPara(r, "oauth2ServerUrl") if err != nil { utils.SendErrorResponse(w, "oauth2ServerUrl not found") @@ -167,6 +187,7 @@ func (ar *OAuth2Router) handleSetOAuthSettingsPOST(w http.ResponseWriter, r *htt ar.options.OAuth2ClientSecret = oauth2ClientSecret ar.options.OAuth2Scopes = oauth2Scopes ar.options.OAuth2CodeChallengeMethod = oauth2CodeChallengeMethod + ar.options.OAuth2ConfigurationCacheTime = oauth2ConfigurationCacheTime //Write changes to database ar.options.Database.Write("oauth2", "oauth2WellKnownUrl", oauth2WellKnownUrl) @@ -177,6 +198,7 @@ func (ar *OAuth2Router) handleSetOAuthSettingsPOST(w http.ResponseWriter, r *htt ar.options.Database.Write("oauth2", "oauth2ClientSecret", oauth2ClientSecret) ar.options.Database.Write("oauth2", "oauth2Scopes", oauth2Scopes) ar.options.Database.Write("oauth2", "oauth2CodeChallengeMethod", oauth2CodeChallengeMethod) + ar.options.Database.Write("oauth2", "oauth2ConfigurationCacheTime", oauth2ConfigurationCacheTime) // Flush caches ar.options.OAuth2ConfigCache.DeleteAll() @@ -202,6 +224,7 @@ func (ar *OAuth2Router) handleSetOAuthSettingsDELETE(w http.ResponseWriter, r *h ar.options.Database.Delete("oauth2", "oauth2ClientSecret") ar.options.Database.Delete("oauth2", "oauth2Scopes") ar.options.Database.Delete("oauth2", "oauth2CodeChallengeMethod") + ar.options.Database.Delete("oauth2", "oauth2ConfigurationCacheTime") utils.SendOK(w) } @@ -273,7 +296,7 @@ func (ar *OAuth2Router) HandleOAuth2Auth(w http.ResponseWriter, r *http.Request) } reqUrl := scheme + "://" + r.Host + r.RequestURI - oauthConfigCache, status := ar.options.OAuth2ConfigCache.GetOrSetFunc(r.Host, func() *oauth2.Config { + oauthConfigCache, _ := ar.options.OAuth2ConfigCache.GetOrSetFunc(r.Host, func() *oauth2.Config { oauthConfig, err := ar.newOAuth2Conf(scheme + "://" + r.Host + callbackPrefix) if err != nil { ar.options.Logger.PrintAndLog("OAuth2Router", "Failed to fetch OIDC configuration:", err) diff --git a/src/mod/utils/utils.go b/src/mod/utils/utils.go index 21d2e40..344df34 100644 --- a/src/mod/utils/utils.go +++ b/src/mod/utils/utils.go @@ -81,6 +81,26 @@ func PostPara(r *http.Request, key string) (string, error) { return x, nil } +// Get POST parameter as time.Duration +func PostDuration(r *http.Request, key string) (*time.Duration, error) { + // Try to parse the form + if err := r.ParseForm(); err != nil { + return nil, err + } + // Get first value from the form + x := r.Form.Get(key) + if len(x) == 0 { + return nil, errors.New("invalid " + key + " given") + } + + duration, err := time.ParseDuration(x) + + if err != nil { + return nil, errors.Join(errors.New("invalid "+key+" duration"), err) + } + return &duration, nil +} + // Get POST paramter as boolean, accept 1 or true func PostBool(r *http.Request, key string) (bool, error) { x, err := PostPara(r, key) diff --git a/src/start.go b/src/start.go index ac1b84a..b2fb0cf 100644 --- a/src/start.go +++ b/src/start.go @@ -188,9 +188,8 @@ func startupSequence() { }) oauth2Router = oauth2.NewOAuth2Router(&oauth2.OAuth2RouterOptions{ - Logger: SystemWideLogger, - Database: sysdb, - OAuth2ConfigCacheTTL: oauth2ConfigurationCache, + Logger: SystemWideLogger, + Database: sysdb, }) //Create a statistic collector diff --git a/src/web/components/sso.html b/src/web/components/sso.html index 85a5ce1..0c7d9d0 100644 --- a/src/web/components/sso.html +++ b/src/web/components/sso.html @@ -158,6 +158,13 @@ Scopes required by the OAuth2 provider to retrieve information about the authenticated user. Refer to your OAuth2 provider documentation for more information about this. Optional if Well-Known url is configured. + +
+ + + Time to cache OAuth2 configuration before refresh. Accepts Go time.Duration format (e.g. 1m, 10m, 1h). Defaults to 60s. +
+ @@ -319,6 +326,7 @@ $('#oauth2ClientId').val(data.oauth2ClientId); $('#oauth2ClientSecret').val(data.oauth2ClientSecret); $('#oauth2Scopes').val(data.oauth2Scopes); + $('#oauth2ConfigurationCacheTime').val(data.oauth2ConfigurationCacheTime); $('[data-value="'+data.oauth2CodeChallengeMethod+'"]').click(); }, error: function(jqXHR, textStatus, errorThrown) {