1010
1111import logging
1212
13- from okta .constants import FINDING_OKTA_DOMAIN , REPO_URL , MIN_DPOP_KEY_ROTATION_SECONDS , MAX_DPOP_KEY_ROTATION_SECONDS
13+ from okta .constants import (
14+ FINDING_OKTA_DOMAIN , LOGGER_NAME , REPO_URL ,
15+ MIN_DPOP_KEY_ROTATION_SECONDS , MAX_DPOP_KEY_ROTATION_SECONDS ,
16+ )
1417from okta .error_messages import (
1518 ERROR_MESSAGE_ORG_URL_MISSING ,
1619 ERROR_MESSAGE_API_TOKEN_DEFAULT ,
2831 ERROR_MESSAGE_PROXY_INVALID_PORT ,
2932)
3033
31- logger = logging .getLogger ("okta-sdk-python" )
34+ logger = logging .getLogger (LOGGER_NAME )
3235
3336
3437class ConfigValidator :
@@ -55,7 +58,6 @@ def validate_config(self):
5558 errors = []
5659 # Defensive: default to empty dict if sections are missing
5760 client = self ._config .get ("client" , {})
58- _ = self ._config .get ("testing" , {})
5961
6062 # check org url
6163 errors += self ._validate_org_url (client .get ("orgUrl" , "" ))
@@ -65,6 +67,14 @@ def validate_config(self):
6567 # check API details based on authorization mode
6668 if client .get ("authorizationMode" ) in ("SSWS" , "Bearer" ):
6769 errors += self ._validate_token (client .get ("token" , "" ))
70+ # Warn if DPoP is enabled with a non-OAuth auth mode (it has no effect)
71+ if client .get ("dpopEnabled" , False ):
72+ logger .warning (
73+ "dpopEnabled is True but authorizationMode is '%s'. "
74+ "DPoP only applies to 'PrivateKey' (OAuth) mode and will "
75+ "be ignored. Set authorizationMode to 'PrivateKey' to use DPoP." ,
76+ client .get ("authorizationMode" ),
77+ )
6878 elif client .get ("authorizationMode" ) == "PrivateKey" :
6979 client_fields = [
7080 "clientId" ,
@@ -237,49 +247,89 @@ def _validate_dpop_config(self, client):
237247 """
238248 Validate DPoP-specific configuration.
239249
250+ Coerces string values from environment variables to their expected
251+ types (bool for ``dpopEnabled``, int for ``dpopKeyRotationInterval``)
252+ before validation. The coerced values are written back into *client*
253+ so that downstream code receives the correct Python types.
254+
240255 Note: This method is only called when authorizationMode is 'PrivateKey',
241256 so no need to re-check the auth mode here.
242257
243258 Args:
244- client: Client configuration dict
259+ client (dict): Client configuration dict (mutated in-place for
260+ type coercion of string values from environment variables).
245261
246262 Returns:
247263 list: List of error messages (empty if valid)
248264 """
249265
250266 errors = []
251267
252- if not client .get ('dpopEnabled' ):
268+ dpop_enabled = client .get ('dpopEnabled' , False )
269+
270+ # Coerce string from env vars to bool (e.g. "true" → True)
271+ if isinstance (dpop_enabled , str ):
272+ dpop_enabled = dpop_enabled .strip ().lower () == 'true'
273+ client ['dpopEnabled' ] = dpop_enabled
274+
275+ # Validate dpopEnabled is a boolean
276+ if not isinstance (dpop_enabled , bool ):
277+ errors .append (
278+ "dpopEnabled must be a boolean (True/False), "
279+ f"but got { type (dpop_enabled ).__name__ } : { dpop_enabled !r} "
280+ )
281+ return errors # Cannot validate further if type is wrong
282+
283+ if not dpop_enabled :
253284 return errors # DPoP not enabled, nothing to validate
254285
255286 # Validate key rotation interval
256287 rotation_interval = client .get ('dpopKeyRotationInterval' , 86400 )
257288
289+ # Coerce string from env vars to int (e.g. "86400" → 86400)
290+ if isinstance (rotation_interval , str ):
291+ try :
292+ rotation_interval = int (rotation_interval )
293+ client ['dpopKeyRotationInterval' ] = rotation_interval
294+ except ValueError :
295+ errors .append (
296+ "dpopKeyRotationInterval must be a valid integer "
297+ f"(seconds), but got non-numeric string: "
298+ f"{ rotation_interval !r} "
299+ )
300+ return errors
301+
258302 if not isinstance (rotation_interval , int ):
259303 errors .append (
260- f "dpopKeyRotationInterval must be an integer (seconds), "
304+ "dpopKeyRotationInterval must be an integer (seconds), "
261305 f"but got { type (rotation_interval ).__name__ } "
262306 )
263- elif rotation_interval < MIN_DPOP_KEY_ROTATION_SECONDS : # Minimum 1 hour
307+ elif rotation_interval < MIN_DPOP_KEY_ROTATION_SECONDS :
264308 errors .append (
265- f"dpopKeyRotationInterval must be at least { MIN_DPOP_KEY_ROTATION_SECONDS } seconds (1 hour), "
309+ "dpopKeyRotationInterval must be at least "
310+ f"{ MIN_DPOP_KEY_ROTATION_SECONDS } seconds (1 hour), "
266311 f"but got { rotation_interval } seconds. "
267312 "Shorter intervals may cause performance issues."
268313 )
269- elif rotation_interval > MAX_DPOP_KEY_ROTATION_SECONDS : # Maximum 90 days
314+ elif rotation_interval > MAX_DPOP_KEY_ROTATION_SECONDS :
315+ max_days = MAX_DPOP_KEY_ROTATION_SECONDS // 86400
316+ given_days = rotation_interval // 86400
270317 errors .append (
271- f"dpopKeyRotationInterval must be at most { MAX_DPOP_KEY_ROTATION_SECONDS } seconds "
272- f"({ MAX_DPOP_KEY_ROTATION_SECONDS // 86400 } days), "
273- f"but got { rotation_interval } seconds ({ rotation_interval // 86400 } days). "
274- "Excessive rotation intervals defeat the security purpose of DPoP. "
318+ "dpopKeyRotationInterval must be at most "
319+ f"{ MAX_DPOP_KEY_ROTATION_SECONDS } seconds "
320+ f"({ max_days } days), but got { rotation_interval } "
321+ f"seconds ({ given_days } days). "
322+ "Excessive rotation intervals defeat the security "
323+ "purpose of DPoP. "
275324 "Recommended: 24-48 hours for production use."
276325 )
277326 elif rotation_interval > 7 * 24 * 3600 : # Warning for > 7 days
278327 # This is a warning, not an error
279328 logger .warning (
280- f"dpopKeyRotationInterval is very long ({ rotation_interval } seconds, "
281- f"{ rotation_interval // 86400 } days). "
282- "Consider shorter intervals (24-48 hours) for better security."
329+ "dpopKeyRotationInterval is very long (%d seconds, "
330+ "%d days). Consider shorter intervals (24-48 hours) "
331+ "for better security." ,
332+ rotation_interval , rotation_interval // 86400 ,
283333 )
284334
285335 return errors
0 commit comments