Repository: nginx/kubernetes-ingress

Issue:
OIDC pkce timeout
Enable the ability to set the timeout for the following:
```
    keyval_zone zone=oidc_pkce:128K        timeout=90s sync;
    keyval_zone zone=oidc_id_tokens:1M     timeout=1h sync;
    keyval_zone zone=oidc_access_tokens:1M timeout=1h sync;
    keyval_zone zone=refresh_tokens:1M     timeout=8h sync;
    keyval_zone zone=oidc_sids:1M          timeout=8h sync;
```

**References**

https://github.com/nginxinc/nginx-openid-connect/issues/110#issuecomment-3215145722

Discussion:

Solution patch:
diff --git a/internal/configs/config_params.go b/internal/configs/config_params.go
index 7dd9c51ae..1410fd45f 100644
--- a/internal/configs/config_params.go
+++ b/internal/configs/config_params.go
@@ -68,6 +68,7 @@ type ConfigParams struct {
 	MainAppProtectDosLogFormat             []string
 	MainAppProtectDosLogFormatEscaping     string
 	MainAppProtectDosArbFqdn               string
+	OIDC                                   OIDC
 	ProxyBuffering                         bool
 	ProxyBuffers                           string
 	ProxyBufferSize                        string
@@ -192,6 +193,15 @@ type ZoneSync struct {
 	ResolverIPV6      *bool
 }
 
+// OIDC holds OIDC configuration parameters.
+type OIDC struct {
+	PKCETimeout    string
+	IDTokenTimeout string
+	AccessTimeout  string
+	RefreshTimeout string
+	SIDSTimeout    string
+}
+
 // MGMTSecrets holds mgmt block secret names
 type MGMTSecrets struct {
 	License     string
@@ -258,6 +268,13 @@ func NewDefaultConfigParams(ctx context.Context, isPlus bool) *ConfigParams {
 		LimitReqZoneSize:              "10m",
 		LimitReqLogLevel:              "error",
 		LimitReqRejectCode:            429,
+		OIDC: OIDC{
+			PKCETimeout:    "90s",
+			IDTokenTimeout: "1h",
+			AccessTimeout:  "1h",
+			RefreshTimeout: "8h",
+			SIDSTimeout:    "8h",
+		},
 	}
 }
 
diff --git a/internal/configs/configmaps.go b/internal/configs/configmaps.go
index ed4d6dc27..c4e514e43 100644
--- a/internal/configs/configmaps.go
+++ b/internal/configs/configmaps.go
@@ -459,6 +459,11 @@ func ParseConfigMap(ctx context.Context, cfgm *v1.ConfigMap, nginxPlus bool, has
 		configOk = false
 	}
 
+	err = parseConfigMapOIDC(l, cfgm, cfgParams, eventLog)
+	if err != nil {
+		configOk = false
+	}
+
 	if upstreamZoneSize, exists := cfgm.Data["upstream-zone-size"]; exists {
 		cfgParams.UpstreamZoneSize = upstreamZoneSize
 	}
@@ -694,6 +699,61 @@ func ParseConfigMap(ctx context.Context, cfgm *v1.ConfigMap, nginxPlus bool, has
 	return cfgParams, configOk
 }
 
+// parseConfigMapOIDC parses OIDC timeout configuration from ConfigMap.
+func parseConfigMapOIDC(l *slog.Logger, cfgm *v1.ConfigMap, cfgParams *ConfigParams, eventLog record.EventRecorder) error {
+	if oidcPKCETimeout, exists := cfgm.Data["oidc-pkce-timeout"]; exists {
+		pkceTimeout, err := ParseTime(oidcPKCETimeout)
+		if err != nil {
+			errorText := fmt.Sprintf("ConfigMap %s/%s: invalid value for 'oidc-pkce-timeout': %q, must be a valid nginx time (e.g. '90s', '5m', '1h')", cfgm.Namespace, cfgm.Name, oidcPKCETimeout)
+			nl.Warn(l, errorText)
+			eventLog.Event(cfgm, v1.EventTypeWarning, nl.EventReasonInvalidValue, errorText)
+			return err
+		}
+		cfgParams.OIDC.PKCETimeout = pkceTimeout
+	}
+	if oidcIDTokensTimeout, exists := cfgm.Data["oidc-id-tokens-timeout"]; exists {
+		idTokensTimeout, err := ParseTime(oidcIDTokensTimeout)
+		if err != nil {
+			errorText := fmt.Sprintf("ConfigMap %s/%s: invalid value for 'oidc-id-tokens-timeout': %q, must be a valid nginx time (e.g. '1h', '30m', '2h')", cfgm.Namespace, cfgm.Name, oidcIDTokensTimeout)
+			nl.Warn(l, errorText)
+			eventLog.Event(cfgm, v1.EventTypeWarning, nl.EventReasonInvalidValue, errorText)
+			return err
+		}
+		cfgParams.OIDC.IDTokenTimeout = idTokensTimeout
+	}
+	if oidcAccessTokensTimeout, exists := cfgm.Data["oidc-access-tokens-timeout"]; exists {
+		accessTokensTimeout, err := ParseTime(oidcAccessTokensTimeout)
+		if err != nil {
+			errorText := fmt.Sprintf("ConfigMap %s/%s: invalid value for 'oidc-access-tokens-timeout': %q, must be a valid nginx time (e.g. '1h', '30m', '2h')", cfgm.Namespace, cfgm.Name, oidcAccessTokensTimeout)
+			nl.Warn(l, errorText)
+			eventLog.Event(cfgm, v1.EventTypeWarning, nl.EventReasonInvalidValue, errorText)
+			return err
+		}
+		cfgParams.OIDC.AccessTimeout = accessTokensTimeout
+	}
+	if oidcRefreshTokensTimeout, exists := cfgm.Data["oidc-refresh-tokens-timeout"]; exists {
+		refreshTokensTimeout, err := ParseTime(oidcRefreshTokensTimeout)
+		if err != nil {
+			errorText := fmt.Sprintf("ConfigMap %s/%s: invalid value for 'oidc-refresh-tokens-timeout': %q, must be a valid nginx time (e.g. '8h', '12h', '24h')", cfgm.Namespace, cfgm.Name, oidcRefreshTokensTimeout)
+			nl.Warn(l, errorText)
+			eventLog.Event(cfgm, v1.EventTypeWarning, nl.EventReasonInvalidValue, errorText)
+			return err
+		}
+		cfgParams.OIDC.RefreshTimeout = refreshTokensTimeout
+	}
+	if oidcSIDSTimeout, exists := cfgm.Data["oidc-sids-timeout"]; exists {
+		sidsTimeout, err := ParseTime(oidcSIDSTimeout)
+		if err != nil {
+			errorText := fmt.Sprintf("ConfigMap %s/%s: invalid value for 'oidc-sids-timeout': %q, must be a valid nginx time (e.g. '8h', '12h', '24h')", cfgm.Namespace, cfgm.Name, oidcSIDSTimeout)
+			nl.Warn(l, errorText)
+			eventLog.Event(cfgm, v1.EventTypeWarning, nl.EventReasonInvalidValue, errorText)
+			return err
+		}
+		cfgParams.OIDC.SIDSTimeout = sidsTimeout
+	}
+	return nil
+}
+
 //nolint:gocyclo
 func parseConfigMapZoneSync(l *slog.Logger, cfgm *v1.ConfigMap, cfgParams *ConfigParams, eventLog record.EventRecorder, nginxPlus bool) (*ZoneSync, error) {
 	if zoneSync, exists, err := GetMapKeyAsBool(cfgm.Data, "zone-sync", cfgm); exists {
@@ -1121,11 +1181,18 @@ func GenerateNginxMainConfig(staticCfgParams *StaticConfigParams, config *Config
 		InternalRouteServer:                staticCfgParams.EnableInternalRoutes,
 		InternalRouteServerName:            staticCfgParams.InternalRouteServerName,
 		LatencyMetrics:                     staticCfgParams.EnableLatencyMetrics,
-		OIDC:                               staticCfgParams.EnableOIDC,
-		ZoneSyncConfig:                     zoneSyncConfig,
-		DynamicSSLReloadEnabled:            staticCfgParams.DynamicSSLReload,
-		StaticSSLPath:                      staticCfgParams.StaticSSLPath,
-		NginxVersion:                       staticCfgParams.NginxVersion,
+		OIDC: version1.OIDCConfig{
+			Enable:         staticCfgParams.EnableOIDC,
+			PKCETimeout:    config.OIDC.PKCETimeout,
+			IDTokenTimeout: config.OIDC.IDTokenTimeout,
+			AccessTimeout:  config.OIDC.AccessTimeout,
+			RefreshTimeout: config.OIDC.RefreshTimeout,
+			SIDSTimeout:    config.OIDC.SIDSTimeout,
+		},
+		ZoneSyncConfig:          zoneSyncConfig,
+		DynamicSSLReloadEnabled: staticCfgParams.DynamicSSLReload,
+		StaticSSLPath:           staticCfgParams.StaticSSLPath,
+		NginxVersion:            staticCfgParams.NginxVersion,
 	}
 	return nginxCfg
 }
diff --git a/internal/configs/oidc/oidc_common.conf b/internal/configs/oidc/oidc_common.conf
index 7b14bf961..208162bb7 100644
--- a/internal/configs/oidc/oidc_common.conf
+++ b/internal/configs/oidc/oidc_common.conf
@@ -16,12 +16,6 @@ map $http_x_forwarded_proto $proto {
 # JWK Set will be fetched from $oidc_jwks_uri and cached here - ensure writable by nginx user
 proxy_cache_path /var/cache/nginx/jwk levels=1 keys_zone=jwk:64k max_size=1m;
 
-# Change timeout values to at least the validity period of each token type
-keyval_zone zone=oidc_id_tokens:1M     timeout=1h sync;
-keyval_zone zone=oidc_access_tokens:1M timeout=1h sync;
-keyval_zone zone=refresh_tokens:1M     timeout=8h sync;
-keyval_zone zone=oidc_sids:1M          timeout=8h sync;
-
 keyval $cookie_auth_token $session_jwt   zone=oidc_id_tokens;     # Exchange cookie for ID token(JWT)
 keyval $cookie_auth_token $access_token  zone=oidc_access_tokens; # Exchange cookie for access token
 keyval $cookie_auth_token $refresh_token zone=refresh_tokens;     # Exchange cookie for refresh token
diff --git a/internal/configs/oidc/oidc_pkce_supplements.conf b/internal/configs/oidc/oidc_pkce_supplements.conf
deleted file mode 100644
index 668b817f4..000000000
--- a/internal/configs/oidc/oidc_pkce_supplements.conf
+++ /dev/null
@@ -1,8 +0,0 @@
-# https://nginx.org/en/docs/http/ngx_http_keyval_module.html
-# context: http
-
-# keyval_zone keyval_zone zone=name:size [state=file] [timeout=time] [type=string|ip|prefix] [sync];
-keyval_zone zone=oidc_pkce:128K timeout=90s sync; # Temporary storage for PKCE code verifier.
-
-# keyval key $variable zone
-keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;
diff --git a/internal/configs/version1/__snapshots__/template_test.snap b/internal/configs/version1/__snapshots__/template_test.snap
index 20e332277..c3982d536 100644
--- a/internal/configs/version1/__snapshots__/template_test.snap
+++ b/internal/configs/version1/__snapshots__/template_test.snap
@@ -3251,6 +3251,294 @@ mgmt {
 
 ---
 
+[TestExecuteTemplate_ForMainForNGINXPlusWithOIDCTimeoutCustom - 1]
+worker_processes  ;
+
+daemon off;
+
+error_log  stderr ;
+pid        /var/lib/nginx/nginx.pid;
+load_module modules/ngx_fips_check_module.so;
+
+load_module modules/ngx_http_js_module.so;
+
+events {
+    worker_connections  ;
+}
+
+http {
+    include       /etc/nginx/mime.types;
+    default_type  application/octet-stream;
+    map_hash_max_size ;
+    map_hash_bucket_size ;
+
+    js_import /etc/nginx/njs/apikey_auth.js;
+    js_set $apikey_auth_hash apikey_auth.hash;
+
+    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
+                      '$status $body_bytes_sent "$http_referer" '
+                      '"$http_user_agent" "$http_x_forwarded_for"';
+
+    map $upstream_trailer_grpc_status $grpc_status {
+        default $upstream_trailer_grpc_status;
+        '' $sent_http_grpc_status;
+    }
+
+    access_log ;
+
+    sendfile        on;
+    #tcp_nopush     on;
+
+    keepalive_timeout ;
+    keepalive_requests 0;
+
+    #gzip  on;
+
+    server_names_hash_max_size ;
+    
+
+    variables_hash_bucket_size 0;
+    variables_hash_max_size 0;
+
+    map $request_uri $request_uri_no_args {
+        "~^(?P<path>[^?]*)(\?.*)?$" $path;
+    }
+
+    map $http_upgrade $connection_upgrade {
+        default upgrade;
+        ''      close;
+    }
+    map $http_upgrade $vs_connection_header {
+        default upgrade;
+        ''      $default_connection_header;
+    }
+
+    
+    
+    keyval_zone zone=oidc_pkce:128K        timeout=2m sync;
+    keyval_zone zone=oidc_id_tokens:1M     timeout=2h sync;
+    keyval_zone zone=oidc_access_tokens:1M timeout=30m sync;
+    keyval_zone zone=refresh_tokens:1M     timeout=1h sync;
+    keyval_zone zone=oidc_sids:1M          timeout=120s sync;
+    include oidc/oidc_common.conf;
+
+    server {
+        # required to support the Websocket protocol in VirtualServer/VirtualServerRoutes
+        set $default_connection_header "";
+        set $resource_type "";
+        set $resource_name "";
+        set $resource_namespace "";
+        set $service "";
+
+        listen 0 default_server;listen [::]:0 default_server;
+        listen 0 ssl default_server;
+        listen [::]:0 ssl default_server;
+        ssl_certificate /etc/nginx/secrets/default;
+        ssl_certificate_key /etc/nginx/secrets/default;
+
+        server_name _;
+        server_tokens "";
+
+        location / {
+            return ;
+        }
+    }
+
+    # NGINX Plus API over unix socket
+    server {
+        listen unix:/var/lib/nginx/nginx-plus-api.sock;
+        access_log off;
+
+        # $config_version_mismatch is defined in /etc/nginx/config-version.conf
+        location /configVersionCheck {
+            if ($config_version_mismatch) {
+                return 503;
+            }
+            return 200;
+        }
+
+        location /api {
+            api write=on;
+        }
+    }
+
+    include /etc/nginx/config-version.conf;
+    include /etc/nginx/conf.d/*.conf;
+
+    server {
+        listen unix:/var/lib/nginx/nginx-418-server.sock;
+        access_log off;
+
+        return 418;
+    }
+}
+
+stream {
+    log_format  stream-main  '$remote_addr [$time_local] '
+                      '$protocol $status $bytes_sent $bytes_received '
+                      '$session_time "$ssl_preread_server_name"';
+
+    access_log  /dev/stdout  stream-main;
+    
+    
+
+    map_hash_max_size ;
+    
+    include /etc/nginx/stream-conf.d/*.conf;
+}
+
+mgmt {
+    license_token /license.jwt;
+    enforce_initial_report off;
+    deployment_context /etc/nginx/reporting/tracking.info;
+}
+
+---
+
+[TestExecuteTemplate_ForMainForNGINXPlusWithOIDCTimeoutDefault - 1]
+worker_processes  ;
+
+daemon off;
+
+error_log  stderr ;
+pid        /var/lib/nginx/nginx.pid;
+load_module modules/ngx_fips_check_module.so;
+
+load_module modules/ngx_http_js_module.so;
+
+events {
+    worker_connections  ;
+}
+
+http {
+    include       /etc/nginx/mime.types;
+    default_type  application/octet-stream;
+    map_hash_max_size ;
+    map_hash_bucket_size ;
+
+    js_import /etc/nginx/njs/apikey_auth.js;
+    js_set $apikey_auth_hash apikey_auth.hash;
+
+    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
+                      '$status $body_bytes_sent "$http_referer" '
+                      '"$http_user_agent" "$http_x_forwarded_for"';
+
+    map $upstream_trailer_grpc_status $grpc_status {
+        default $upstream_trailer_grpc_status;
+        '' $sent_http_grpc_status;
+    }
+
+    access_log ;
+
+    sendfile        on;
+    #tcp_nopush     on;
+
+    keepalive_timeout ;
+    keepalive_requests 0;
+
+    #gzip  on;
+
+    server_names_hash_max_size ;
+    
+
+    variables_hash_bucket_size 0;
+    variables_hash_max_size 0;
+
+    map $request_uri $request_uri_no_args {
+        "~^(?P<path>[^?]*)(\?.*)?$" $path;
+    }
+
+    map $http_upgrade $connection_upgrade {
+        default upgrade;
+        ''      close;
+    }
+    map $http_upgrade $vs_connection_header {
+        default upgrade;
+        ''      $default_connection_header;
+    }
+
+    
+    
+    keyval_zone zone=oidc_pkce:128K        timeout=90s sync;
+    keyval_zone zone=oidc_id_tokens:1M     timeout=1h sync;
+    keyval_zone zone=oidc_access_tokens:1M timeout=1h sync;
+    keyval_zone zone=refresh_tokens:1M     timeout=8h sync;
+    keyval_zone zone=oidc_sids:1M          timeout=8h sync;
+    include oidc/oidc_common.conf;
+
+    server {
+        # required to support the Websocket protocol in VirtualServer/VirtualServerRoutes
+        set $default_connection_header "";
+        set $resource_type "";
+        set $resource_name "";
+        set $resource_namespace "";
+        set $service "";
+
+        listen 0 default_server;listen [::]:0 default_server;
+        listen 0 ssl default_server;
+        listen [::]:0 ssl default_server;
+        ssl_certificate /etc/nginx/secrets/default;
+        ssl_certificate_key /etc/nginx/secrets/default;
+
+        server_name _;
+        server_tokens "";
+
+        location / {
+            return ;
+        }
+    }
+
+    # NGINX Plus API over unix socket
+    server {
+        listen unix:/var/lib/nginx/nginx-plus-api.sock;
+        access_log off;
+
+        # $config_version_mismatch is defined in /etc/nginx/config-version.conf
+        location /configVersionCheck {
+            if ($config_version_mismatch) {
+                return 503;
+            }
+            return 200;
+        }
+
+        location /api {
+            api write=on;
+        }
+    }
+
+    include /etc/nginx/config-version.conf;
+    include /etc/nginx/conf.d/*.conf;
+
+    server {
+        listen unix:/var/lib/nginx/nginx-418-server.sock;
+        access_log off;
+
+        return 418;
+    }
+}
+
+stream {
+    log_format  stream-main  '$remote_addr [$time_local] '
+                      '$protocol $status $bytes_sent $bytes_received '
+                      '$session_time "$ssl_preread_server_name"';
+
+    access_log  /dev/stdout  stream-main;
+    
+    
+
+    map_hash_max_size ;
+    
+    include /etc/nginx/stream-conf.d/*.conf;
+}
+
+mgmt {
+    license_token /license.jwt;
+    enforce_initial_report off;
+    deployment_context /etc/nginx/reporting/tracking.info;
+}
+
+---
+
 [TestExecuteTemplate_ForMainForNGINXPlusWithoutCustomDefaultHTTPAndHTTPSListenerPorts - 1]
 worker_processes  auto;
 worker_rlimit_nofile 65536;
diff --git a/internal/configs/version1/config.go b/internal/configs/version1/config.go
index d492697df..30cea8686 100644
--- a/internal/configs/version1/config.go
+++ b/internal/configs/version1/config.go
@@ -204,6 +204,16 @@ type ZoneSyncConfig struct {
 	ResolverIPV6  *bool
 }
 
+// OIDCConfig allows to configure OIDC parameters.
+type OIDCConfig struct {
+	Enable         bool
+	PKCETimeout    string
+	IDTokenTimeout string
+	AccessTimeout  string
+	RefreshTimeout string
+	SIDSTimeout    string
+}
+
 // MGMTConfig is tbe configuration for the MGMT block.
 type MGMTConfig struct {
 	SSLVerify            *bool
@@ -297,7 +307,7 @@ type MainConfig struct {
 	InternalRouteServerName            string
 	LatencyMetrics                     bool
 	ZoneSyncConfig                     ZoneSyncConfig
-	OIDC                               bool
+	OIDC                               OIDCConfig
 	DynamicSSLReloadEnabled            bool
 	StaticSSLPath                      string
 	NginxVersion                       nginx.Version
diff --git a/internal/configs/version1/nginx-plus.tmpl b/internal/configs/version1/nginx-plus.tmpl
index 8ce5b4f00..79bb9b015 100644
--- a/internal/configs/version1/nginx-plus.tmpl
+++ b/internal/configs/version1/nginx-plus.tmpl
@@ -162,7 +162,12 @@ http {
     {{ makeResolver .ResolverAddresses .ResolverValid $resolverIPV6HTTPBool }}
     {{if .ResolverTimeout}}resolver_timeout {{.ResolverTimeout}};{{end}}
 
-    {{- if .OIDC}}
+    {{- if .OIDC.Enable}}
+    keyval_zone zone=oidc_pkce:128K        timeout={{.OIDC.PKCETimeout}} sync;
+    keyval_zone zone=oidc_id_tokens:1M     timeout={{.OIDC.IDTokenTimeout}} sync;
+    keyval_zone zone=oidc_access_tokens:1M timeout={{.OIDC.AccessTimeout}} sync;
+    keyval_zone zone=refresh_tokens:1M     timeout={{.OIDC.RefreshTimeout}} sync;
+    keyval_zone zone=oidc_sids:1M          timeout={{.OIDC.SIDSTimeout}} sync;
     include oidc/oidc_common.conf;
     {{- end}}
 
diff --git a/internal/configs/version2/__snapshots__/templates_test.snap b/internal/configs/version2/__snapshots__/templates_test.snap
index 77f4e238f..50f2fe69b 100644
--- a/internal/configs/version2/__snapshots__/templates_test.snap
+++ b/internal/configs/version2/__snapshots__/templates_test.snap
@@ -1471,7 +1471,7 @@ server {
 [TestExecuteVirtualServerTemplateWithNGINXDebugLevelDebug - 1]
 
 keyval $idp_sid $client_sid              zone=oidc_sids;
-include oidc/oidc_pkce_supplements.conf;
+keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;
 
 server {
     listen 80 proxy_protocol;
@@ -1521,7 +1521,7 @@ server {
 [TestExecuteVirtualServerTemplateWithNGINXDebugLevelNotDebug - 1]
 
 keyval $idp_sid $client_sid              zone=oidc_sids;
-include oidc/oidc_pkce_supplements.conf;
+keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;
 
 server {
     listen 80 proxy_protocol;
@@ -1570,7 +1570,7 @@ server {
 [TestExecuteVirtualServerTemplateWithOIDCAndPKCEPolicyNGINXPlus - 1]
 
 keyval $idp_sid $client_sid              zone=oidc_sids;
-include oidc/oidc_pkce_supplements.conf;
+keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;
 
 server {
     listen 80 proxy_protocol;
diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl
index 91b652c4d..4886cc246 100644
--- a/internal/configs/version2/nginx-plus.virtualserver.tmpl
+++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl
@@ -118,7 +118,7 @@ map $request_method $cache_purge_{{ replaceAll $l.Cache.ZoneName "-" "_" }} {
 {{- if $s.OIDC  }}
 keyval $idp_sid $client_sid              zone=oidc_sids;
 {{- if $s.OIDC.PKCEEnable }}
-include oidc/oidc_pkce_supplements.conf;
+keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;
 {{- end }}
 {{- end }}
 



Test patch:
diff --git a/internal/configs/configmaps_test.go b/internal/configs/configmaps_test.go
index 503934029..30e054f3b 100644
--- a/internal/configs/configmaps_test.go
+++ b/internal/configs/configmaps_test.go
@@ -298,6 +298,246 @@ func TestParseConfigMapAccessLogDefault(t *testing.T) {
 	}
 }
 
+func TestParseConfigMapOIDC(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		configMap *v1.ConfigMap
+		want      *OIDC
+		msg       string
+	}{
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{},
+			},
+			want: &OIDC{
+				PKCETimeout:    "90s",
+				IDTokenTimeout: "1h",
+				AccessTimeout:  "1h",
+				RefreshTimeout: "8h",
+				SIDSTimeout:    "8h",
+			},
+			msg: "default OIDC values",
+		},
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-pkce-timeout":           "5m",
+					"oidc-id-tokens-timeout":      "2h",
+					"oidc-access-tokens-timeout":  "3h",
+					"oidc-refresh-tokens-timeout": "48h",
+					"oidc-sids-timeout":           "72h",
+				},
+			},
+			want: &OIDC{
+				PKCETimeout:    "5m",
+				IDTokenTimeout: "2h",
+				AccessTimeout:  "3h",
+				RefreshTimeout: "48h",
+				SIDSTimeout:    "72h",
+			},
+			msg: "all timeout values custom",
+		},
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-pkce-timeout": "15m",
+				},
+			},
+			want: &OIDC{
+				PKCETimeout: "15m",
+			},
+			msg: "custom PKCE timeout only",
+		},
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-id-tokens-timeout": "90m",
+				},
+			},
+			want: &OIDC{
+				IDTokenTimeout: "90m",
+			},
+			msg: "custom ID token timeout only",
+		},
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-access-tokens-timeout": "4h",
+				},
+			},
+			want: &OIDC{
+				AccessTimeout: "4h",
+			},
+			msg: "custom access token timeout only",
+		},
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-refresh-tokens-timeout": "16h",
+				},
+			},
+			want: &OIDC{
+				RefreshTimeout: "16h",
+			},
+			msg: "custom refresh token timeout only",
+		},
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-sids-timeout": "12h",
+				},
+			},
+			want: &OIDC{
+				SIDSTimeout: "12h",
+			},
+			msg: "custom SIDS timeout only",
+		},
+	}
+
+	nginxPlus := true
+	hasAppProtect := false
+	hasAppProtectDos := false
+	hasTLSPassthrough := false
+	directiveAutoadjustEnabled := false
+
+	for _, test := range tests {
+		t.Run(test.msg, func(t *testing.T) {
+			result, configOk := ParseConfigMap(context.Background(), test.configMap, nginxPlus, hasAppProtect, hasAppProtectDos, hasTLSPassthrough, directiveAutoadjustEnabled, makeEventLogger())
+			if !configOk {
+				t.Error("want configOk true, got configOk false")
+			}
+
+			// Check only the specific fields that are set in the test expectation
+			if test.want.PKCETimeout != "" {
+				assert.Equal(t, test.want.PKCETimeout, result.OIDC.PKCETimeout)
+			}
+			if test.want.IDTokenTimeout != "" {
+				assert.Equal(t, test.want.IDTokenTimeout, result.OIDC.IDTokenTimeout)
+			}
+			if test.want.AccessTimeout != "" {
+				assert.Equal(t, test.want.AccessTimeout, result.OIDC.AccessTimeout)
+			}
+			if test.want.RefreshTimeout != "" {
+				assert.Equal(t, test.want.RefreshTimeout, result.OIDC.RefreshTimeout)
+			}
+			if test.want.SIDSTimeout != "" {
+				assert.Equal(t, test.want.SIDSTimeout, result.OIDC.SIDSTimeout)
+			}
+		})
+	}
+}
+
+func TestParseConfigMapOIDCErrors(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		configMap   *v1.ConfigMap
+		expectedErr bool
+		msg         string
+	}{
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-pkce-timeout": "invalid-time",
+				},
+			},
+			expectedErr: true,
+			msg:         "invalid PKCE timeout format",
+		},
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-id-tokens-timeout": "abc123",
+				},
+			},
+			expectedErr: true,
+			msg:         "invalid ID token timeout format",
+		},
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-access-tokens-timeout": "5x",
+				},
+			},
+			expectedErr: true,
+			msg:         "invalid access token timeout format",
+		},
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-refresh-tokens-timeout": "",
+				},
+			},
+			expectedErr: true,
+			msg:         "empty refresh token timeout",
+		},
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-sids-timeout": "   ",
+				},
+			},
+			expectedErr: true,
+			msg:         "whitespace-only SIDS timeout",
+		},
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-pkce-timeout": "-5m",
+				},
+			},
+			expectedErr: true,
+			msg:         "negative PKCE timeout",
+		},
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-id-tokens-timeout": "1.5h",
+				},
+			},
+			expectedErr: true,
+			msg:         "decimal in ID token timeout",
+		},
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-access-tokens-timeout": "5minutes",
+				},
+			},
+			expectedErr: true,
+			msg:         "invalid time unit format",
+		},
+
+		{
+			configMap: &v1.ConfigMap{
+				Data: map[string]string{
+					"oidc-sids-timeout": "5s 10m",
+				},
+			},
+			expectedErr: true,
+			msg:         "multiple time values without proper format",
+		},
+	}
+
+	nginxPlus := true
+	hasAppProtect := false
+	hasAppProtectDos := false
+	hasTLSPassthrough := false
+	directiveAutoadjustEnabled := false
+
+	for _, test := range tests {
+		t.Run(test.msg, func(t *testing.T) {
+			_, configOk := ParseConfigMap(context.Background(), test.configMap, nginxPlus, hasAppProtect, hasAppProtectDos, hasTLSPassthrough, directiveAutoadjustEnabled, makeEventLogger())
+
+			if test.expectedErr && configOk {
+				t.Errorf("want configOk false, got configOk true for %s", test.msg)
+			}
+			if !test.expectedErr && !configOk {
+				t.Errorf("want configOk true, got configOk false for %s", test.msg)
+			}
+		})
+	}
+}
+
 func TestParseMGMTConfigMapError(t *testing.T) {
 	t.Parallel()
 	tests := []struct {
diff --git a/internal/configs/version1/template_test.go b/internal/configs/version1/template_test.go
index 4a4ed277f..efc8f015c 100644
--- a/internal/configs/version1/template_test.go
+++ b/internal/configs/version1/template_test.go
@@ -790,6 +790,68 @@ func TestExecuteTemplate_ForMainForNGINXPlusWithCustomDefaultHTTPSListenerPort(t
 	snaps.MatchSnapshot(t, buf.String())
 }
 
+func TestExecuteTemplate_ForMainForNGINXPlusWithOIDCTimeoutDefault(t *testing.T) {
+	t.Parallel()
+
+	tmpl := newNGINXPlusMainTmpl(t)
+	buf := &bytes.Buffer{}
+
+	err := tmpl.Execute(buf, mainCfgWithOIDCTimeoutDefault)
+	if err != nil {
+		t.Fatalf("Failed to write template %v", err)
+	}
+
+	mainConf := buf.String()
+	t.Log(mainConf)
+
+	expectedDirectives := []string{
+		"keyval_zone zone=oidc_pkce:128K        timeout=90s sync;",
+		"keyval_zone zone=oidc_id_tokens:1M     timeout=1h sync;",
+		"keyval_zone zone=oidc_access_tokens:1M timeout=1h sync;",
+		"keyval_zone zone=refresh_tokens:1M     timeout=8h sync;",
+		"keyval_zone zone=oidc_sids:1M          timeout=8h sync;",
+		"include oidc/oidc_common.conf;",
+	}
+
+	for _, directive := range expectedDirectives {
+		if !strings.Contains(mainConf, directive) {
+			t.Errorf("oidc timeout directive not found: %s", directive)
+		}
+	}
+	snaps.MatchSnapshot(t, buf.String())
+}
+
+func TestExecuteTemplate_ForMainForNGINXPlusWithOIDCTimeoutCustom(t *testing.T) {
+	t.Parallel()
+
+	tmpl := newNGINXPlusMainTmpl(t)
+	buf := &bytes.Buffer{}
+
+	err := tmpl.Execute(buf, mainCfgWithOIDCTimeoutCustom)
+	if err != nil {
+		t.Fatalf("Failed to write template %v", err)
+	}
+
+	mainConf := buf.String()
+	t.Log(mainConf)
+
+	expectedDirectives := []string{
+		"keyval_zone zone=oidc_pkce:128K        timeout=2m sync;",
+		"keyval_zone zone=oidc_id_tokens:1M     timeout=2h sync;",
+		"keyval_zone zone=oidc_access_tokens:1M timeout=30m sync;",
+		"keyval_zone zone=refresh_tokens:1M     timeout=1h sync;",
+		"keyval_zone zone=oidc_sids:1M          timeout=120s sync;",
+		"include oidc/oidc_common.conf;",
+	}
+
+	for _, directive := range expectedDirectives {
+		if !strings.Contains(mainConf, directive) {
+			t.Errorf("oidc timeout directive not found: %s", directive)
+		}
+	}
+	snaps.MatchSnapshot(t, buf.String())
+}
+
 func TestExecuteTemplate_ForMainForNGINXWithHTTP2On(t *testing.T) {
 	t.Parallel()
 
@@ -2628,6 +2690,28 @@ var (
 		MainOtelServiceName:         "nginx-ingress-controller:nginx",
 	}
 
+	mainCfgWithOIDCTimeoutDefault = MainConfig{
+		OIDC: OIDCConfig{
+			Enable:         true,
+			PKCETimeout:    "90s",
+			IDTokenTimeout: "1h",
+			AccessTimeout:  "1h",
+			RefreshTimeout: "8h",
+			SIDSTimeout:    "8h",
+		},
+	}
+
+	mainCfgWithOIDCTimeoutCustom = MainConfig{
+		OIDC: OIDCConfig{
+			Enable:         true,
+			PKCETimeout:    "2m",
+			IDTokenTimeout: "2h",
+			AccessTimeout:  "30m",
+			RefreshTimeout: "1h",
+			SIDSTimeout:    "120s",
+		},
+	}
+
 	// Vars for Mergable Ingress Master - Minion tests
 
 	coffeeUpstreamNginxPlus = Upstream{
diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go
index 3f565a9e0..7b5253f92 100644
--- a/internal/configs/version2/templates_test.go
+++ b/internal/configs/version2/templates_test.go
@@ -848,7 +848,7 @@ func TestExecuteVirtualServerTemplateWithOIDCAndPKCEPolicyNGINXPlus(t *testing.T
 		t.Error(err)
 	}
 
-	want := "include oidc/oidc_pkce_supplements.conf"
+	want := "keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;"
 	want2 := "include oidc/oidc.conf;"
 
 	if !bytes.Contains(got, []byte(want)) {