Skip to content

Auth

auth

Solution-wide authentication infrastructure.

This module provides authentication functionality including: - Token management (refresh and access tokens) - Keycloak integration for token refresh - Keyring storage for secure token persistence - JWT validation and parsing - Authentication handler that orchestrates all auth operations - CLI command for managing authentication tokens

ApiKeyNotFoundError

Bases: NexusError

Raised when no authentication api key is found.

Source code in src/miura/auth/exceptions.py
class ApiKeyNotFoundError(NexusError):
    """Raised when no authentication api key is found."""

    def __init__(
        self,
        reason: Optional[str] = None,
        details: Optional[Dict[str, Any]] = None,
    ) -> None:
        """
        Initialize an ApiKeyNotFoundError.

        Args:
            reason: Optional custom error message
            details: Optional dictionary with additional context
        """
        default_reason = (
            "No authentication api key found. "
            "Use 'miura-auth token set' or set MIURA_NEXUS_API_KEY environment variable."
        )
        super().__init__(ErrorCode.AUTH_ERROR, reason or default_reason, details)

_redact_sensitive_data

_redact_sensitive_data() -> None

Redact sensitive information from details dictionary.

Source code in src/miura/sdk/exceptions.py
def _redact_sensitive_data(self) -> None:
    """Redact sensitive information from details dictionary."""
    sensitive_keys = ["token", "password", "secret", "credential", "key", "auth"]
    for key in list(self.details.keys()):
        key_lower = key.lower()
        if any(sensitive in key_lower for sensitive in sensitive_keys):
            self.details[key] = "[REDACTED]"

__init__

__init__(reason: Optional[str] = None, details: Optional[Dict[str, Any]] = None) -> None

Initialize an ApiKeyNotFoundError.

Parameters:

Name Type Description Default
reason Optional[str]

Optional custom error message

None
details Optional[Dict[str, Any]]

Optional dictionary with additional context

None
Source code in src/miura/auth/exceptions.py
def __init__(
    self,
    reason: Optional[str] = None,
    details: Optional[Dict[str, Any]] = None,
) -> None:
    """
    Initialize an ApiKeyNotFoundError.

    Args:
        reason: Optional custom error message
        details: Optional dictionary with additional context
    """
    default_reason = (
        "No authentication api key found. "
        "Use 'miura-auth token set' or set MIURA_NEXUS_API_KEY environment variable."
    )
    super().__init__(ErrorCode.AUTH_ERROR, reason or default_reason, details)

ApiKeyExpiredError

Bases: NexusError

Raised when the authentication api key has expired.

Source code in src/miura/auth/exceptions.py
class ApiKeyExpiredError(NexusError):
    """Raised when the authentication api key has expired."""

    def __init__(
        self,
        reason: Optional[str] = None,
        details: Optional[Dict[str, Any]] = None,
    ) -> None:
        """
        Initialize an ApiKeyExpiredError.

        Args:
            reason: Optional custom error message
            details: Optional dictionary with additional context
        """
        default_reason = "Authentication api key has expired. " "Use 'miura-auth token set' to set a new api key."
        super().__init__(ErrorCode.TOKEN_EXPIRED, reason or default_reason, details)

_redact_sensitive_data

_redact_sensitive_data() -> None

Redact sensitive information from details dictionary.

Source code in src/miura/sdk/exceptions.py
def _redact_sensitive_data(self) -> None:
    """Redact sensitive information from details dictionary."""
    sensitive_keys = ["token", "password", "secret", "credential", "key", "auth"]
    for key in list(self.details.keys()):
        key_lower = key.lower()
        if any(sensitive in key_lower for sensitive in sensitive_keys):
            self.details[key] = "[REDACTED]"

__init__

__init__(reason: Optional[str] = None, details: Optional[Dict[str, Any]] = None) -> None

Initialize an ApiKeyExpiredError.

Parameters:

Name Type Description Default
reason Optional[str]

Optional custom error message

None
details Optional[Dict[str, Any]]

Optional dictionary with additional context

None
Source code in src/miura/auth/exceptions.py
def __init__(
    self,
    reason: Optional[str] = None,
    details: Optional[Dict[str, Any]] = None,
) -> None:
    """
    Initialize an ApiKeyExpiredError.

    Args:
        reason: Optional custom error message
        details: Optional dictionary with additional context
    """
    default_reason = "Authentication api key has expired. " "Use 'miura-auth token set' to set a new api key."
    super().__init__(ErrorCode.TOKEN_EXPIRED, reason or default_reason, details)

ApiKeyInvalidError

Bases: NexusError

Raised when the authentication api key is invalid or malformed.

Source code in src/miura/auth/exceptions.py
class ApiKeyInvalidError(NexusError):
    """Raised when the authentication api key is invalid or malformed."""

    def __init__(
        self,
        reason: Optional[str] = None,
        details: Optional[Dict[str, Any]] = None,
    ) -> None:
        """
        Initialize an ApiKeyInvalidError.

        Args:
            reason: Optional custom error message
            details: Optional dictionary with additional context
        """
        default_reason = (
            "Authentication api key is invalid or malformed. " "Check the token format and ensure it's a valid JWT."
        )
        super().__init__(ErrorCode.TOKEN_INVALID, reason or default_reason, details)

_redact_sensitive_data

_redact_sensitive_data() -> None

Redact sensitive information from details dictionary.

Source code in src/miura/sdk/exceptions.py
def _redact_sensitive_data(self) -> None:
    """Redact sensitive information from details dictionary."""
    sensitive_keys = ["token", "password", "secret", "credential", "key", "auth"]
    for key in list(self.details.keys()):
        key_lower = key.lower()
        if any(sensitive in key_lower for sensitive in sensitive_keys):
            self.details[key] = "[REDACTED]"

__init__

__init__(reason: Optional[str] = None, details: Optional[Dict[str, Any]] = None) -> None

Initialize an ApiKeyInvalidError.

Parameters:

Name Type Description Default
reason Optional[str]

Optional custom error message

None
details Optional[Dict[str, Any]]

Optional dictionary with additional context

None
Source code in src/miura/auth/exceptions.py
def __init__(
    self,
    reason: Optional[str] = None,
    details: Optional[Dict[str, Any]] = None,
) -> None:
    """
    Initialize an ApiKeyInvalidError.

    Args:
        reason: Optional custom error message
        details: Optional dictionary with additional context
    """
    default_reason = (
        "Authentication api key is invalid or malformed. " "Check the token format and ensure it's a valid JWT."
    )
    super().__init__(ErrorCode.TOKEN_INVALID, reason or default_reason, details)

KeyringError

Bases: NexusError

Raised when there's an error accessing the system keyring.

Source code in src/miura/auth/exceptions.py
class KeyringError(NexusError):
    """Raised when there's an error accessing the system keyring."""

    def __init__(
        self,
        reason: Optional[str] = None,
        details: Optional[Dict[str, Any]] = None,
    ) -> None:
        """
        Initialize a KeyringError.

        Args:
            reason: Optional custom error message
            details: Optional dictionary with additional context
        """
        default_reason = (
            "Failed to access system keyring. "
            "Install a keyring backend (e.g., keyrings.alt) or use environment variables."
        )
        super().__init__(ErrorCode.KEYRING_ERROR, reason or default_reason, details)

_redact_sensitive_data

_redact_sensitive_data() -> None

Redact sensitive information from details dictionary.

Source code in src/miura/sdk/exceptions.py
def _redact_sensitive_data(self) -> None:
    """Redact sensitive information from details dictionary."""
    sensitive_keys = ["token", "password", "secret", "credential", "key", "auth"]
    for key in list(self.details.keys()):
        key_lower = key.lower()
        if any(sensitive in key_lower for sensitive in sensitive_keys):
            self.details[key] = "[REDACTED]"

__init__

__init__(reason: Optional[str] = None, details: Optional[Dict[str, Any]] = None) -> None

Initialize a KeyringError.

Parameters:

Name Type Description Default
reason Optional[str]

Optional custom error message

None
details Optional[Dict[str, Any]]

Optional dictionary with additional context

None
Source code in src/miura/auth/exceptions.py
def __init__(
    self,
    reason: Optional[str] = None,
    details: Optional[Dict[str, Any]] = None,
) -> None:
    """
    Initialize a KeyringError.

    Args:
        reason: Optional custom error message
        details: Optional dictionary with additional context
    """
    default_reason = (
        "Failed to access system keyring. "
        "Install a keyring backend (e.g., keyrings.alt) or use environment variables."
    )
    super().__init__(ErrorCode.KEYRING_ERROR, reason or default_reason, details)

AuthHandler

Main authentication handler that orchestrates all auth operations.

Source code in src/miura/auth/handler.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
class AuthHandler:
    """Main authentication handler that orchestrates all auth operations."""

    def __init__(
        self,
        config_manager: Optional[ConfigurationManager] = None,
        storage_backend: Optional[str] = None,
        verify_ssl: Optional[bool] = None,
    ):
        """
        Initialize authentication handler.

        Args:
            config_manager: Optional ConfigurationManager instance
            storage_backend: Storage backend type ("file", "keyring", or "none"). Defaults to "file".
            verify_ssl: Whether to verify SSL certificates. If None, reads from
                MIURA_KEYCLOAK_VERIFY_SSL env var (default: True)
        """
        # If verify_ssl not explicitly provided, read from environment variable
        if verify_ssl is None:
            verify_ssl = True
            verify_ssl_env = os.environ.get("MIURA_KEYCLOAK_VERIFY_SSL", "").lower()
            if verify_ssl_env in ("false", "0", "no", "off"):
                verify_ssl = False

        self._config_manager = config_manager
        self.token_manager = TokenManager()
        self._keyring_manager: Optional[Any] = None  # Lazy initialization - only create when needed
        self.jwt_utils = JWTUtils()
        self.keycloak_client = KeycloakClient(config_manager=config_manager, verify_ssl=verify_ssl)

        # Determine storage backend
        if storage_backend is None:
            # Default to file storage
            storage_backend = os.environ.get("MIURA_AUTH_STORAGE", "file")

        # Create storage backend
        if storage_backend == "keyring":
            # Only create KeyringManager when using keyring backend
            from .keyring import KeyringManager

            self._keyring_manager = KeyringManager()
            self.storage_backend = create_storage_backend("keyring", self._keyring_manager)
        elif storage_backend == "none":
            self.storage_backend = create_storage_backend("none")
        else:
            # Default to file - no need to create KeyringManager
            self.storage_backend = create_storage_backend("file")

        # Keyring cache (read once per command execution)
        self._keyring_cache: Optional[Dict[str, str]] = None
        self._keyring_cache_initialized = False

    def _get_keyring_cache(self) -> Dict[str, str]:
        """
        Get keyring cache (read once per command execution).

        Returns:
            Dictionary of cached keyring values
        """
        if not self._keyring_cache_initialized:
            self._keyring_cache = {}
            # Only try to read from keyring if using keyring backend
            from .storage import KeyringStorageBackend

            if isinstance(self.storage_backend, KeyringStorageBackend):
                try:
                    # KeyringManager is guaranteed to exist when using keyring backend
                    assert self._keyring_manager is not None
                    token = self._keyring_manager.get_password("auth_api_key")
                    if token:
                        self._keyring_cache["auth_api_key"] = token
                except Exception:
                    # Keyring not available or error - cache remains empty
                    pass
            self._keyring_cache_initialized = True
        # After initialization, _keyring_cache is guaranteed to be a dict
        assert self._keyring_cache is not None
        return self._keyring_cache

    def _clear_keyring_cache(self) -> None:
        """Clear keyring cache (call on command completion)."""
        self._keyring_cache = None
        self._keyring_cache_initialized = False

    def _sync_refresh_token_from_sources(self, storage_backend_type: Optional[str] = None) -> Optional[str]:
        """
        Ensure the in-memory refresh token reflects the latest value from sources.

        Resolution order:
        1. Environment variable (MIURA_NEXUS_API_KEY) - highest priority
        2. Storage backend (file, keyring, or none) - based on --storage flag
        3. Keyring (if --storage=keyring or MIURA_AUTH_STORAGE=keyring)

        Args:
            storage_backend_type: Optional override for storage backend type

        Returns:
            Optional[str]: The current refresh token if available, otherwise None.

        Raises:
            ApiKeyInvalidError: If a token from any source is malformed.
            KeyringError: If keyring access fails unexpectedly.
        """
        # Lazy import to avoid slow startup times
        from miura.nexus.infrastructure.config import ConfigurationManager

        try:
            # Trigger environment loading (if needed)
            ConfigurationManager(load_dotenv=False)
        except Exception as exc:
            logger.debug(f"Could not load application config during token sync: {exc}")

        current_token: Optional[str] = None
        token_source: Optional[str] = None

        # 1. Environment variable has highest priority
        env_refresh_token = os.environ.get("MIURA_NEXUS_API_KEY")
        if env_refresh_token:
            self.jwt_utils.validate_jwt_format(env_refresh_token)
            current_token = env_refresh_token
            token_source = "env"
        else:
            # 2. Try storage backend (file, keyring, or none)
            try:
                stored_token = self.storage_backend.get("auth_api_key")
                if stored_token:
                    self.jwt_utils.validate_jwt_format(stored_token)
                    current_token = stored_token
                    token_source = "storage"
            except Exception as e:
                logger.debug(f"Storage backend access failed: {e}")

            # 3. If using keyring backend and storage didn't work, try keyring cache
            if not current_token and storage_backend_type == "keyring":
                try:
                    cache = self._get_keyring_cache()
                    cached_token = cache.get("auth_api_key")
                    if cached_token:
                        self.jwt_utils.validate_jwt_format(cached_token)
                        current_token = cached_token
                        token_source = "keyring_cache"
                except Exception as e:
                    logger.debug(f"Keyring cache access failed: {e}")

        if current_token is not None:
            refresh_changed = current_token != self.token_manager.refresh_token
            self.token_manager.set_refresh_token(
                current_token,
                reason="Refresh token changed, invalidating cached access token",
            )
            if refresh_changed:
                if token_source == "env":
                    logger.debug("Using refresh token from environment variable (MIURA_NEXUS_API_KEY)")
                elif token_source == "storage":
                    logger.debug(f"Using refresh token from {type(self.storage_backend).__name__} storage")
                elif token_source == "keyring_cache":
                    logger.debug("Using refresh token from keyring cache")
            return self.token_manager.refresh_token

        # No token found in any source; clear cached tokens to avoid stale values
        if self.token_manager.has_tokens():
            logger.debug("No refresh token available, clearing cached tokens")
            self.token_manager.set_refresh_token(
                None,
                reason="Refresh token cleared; removing cached access token",
                force_invalidate=True,
            )
        else:
            self.token_manager.set_refresh_token(None)
        return None

    def get_auth_api_key(self) -> str:
        """
        Get an access token for API authentication.
        Uses refresh token to fetch fresh access tokens transparently.
        Priority order: 1) Environment variable, 2) Memory cache, 3) Keyring.

        Returns:
            str: The access token for API authentication

        Raises:
            ApiKeyNotFoundError: If no refresh token is found in any source
            KeyringError: If there's an error accessing keyring
            ApiKeyInvalidError: If refresh token format is invalid
            PermissionError: If token refresh fails
        """
        # Ensure cached refresh token reflects latest source state
        # Determine storage backend type from handler
        from .storage import KeyringStorageBackend, FileStorageBackend, NoneStorageBackend

        storage_type = None
        if isinstance(self.storage_backend, KeyringStorageBackend):
            storage_type = "keyring"
        elif isinstance(self.storage_backend, FileStorageBackend):
            storage_type = "file"
        elif isinstance(self.storage_backend, NoneStorageBackend):
            storage_type = "none"
        refresh_token = self._sync_refresh_token_from_sources(storage_backend_type=storage_type)

        # Check if we have a valid access token in memory first
        if self.token_manager.access_token:
            # Check if the access token has expired
            try:
                if not self.jwt_utils.is_expired(self.token_manager.access_token):
                    logger.debug("Using cached access token")
                    return self.token_manager.access_token
                else:
                    logger.debug("Access token expired or expiring soon, refreshing...")
            except Exception as e:
                logger.debug(f"Could not check access token expiration: {e}, refreshing...")

            # If we reach here, the token is expired or we couldn't check it
            self.token_manager.invalidate_access_token("Access token expired or could not be verified; clearing cache")
        else:
            logger.debug("Access token cache empty; fetching fresh token")

        # Now use the refresh token to get an access token
        if refresh_token:
            access_token = self.keycloak_client.refresh_access_token(refresh_token)
            self.token_manager.set_access_token(access_token)
            return access_token

        # Determine which error to surface for missing refresh token
        if os.environ.get("MIURA_NEXUS_API_KEY"):
            raise ApiKeyInvalidError(
                "MIURA_NEXUS_API_KEY is set but could not be validated",
                details={"source": "environment_variable"},
            )

        # Note: Keyring availability is checked lazily when operations are attempted
        # If keyring operations fail, appropriate errors will be raised

        raise ApiKeyNotFoundError(
            "No refresh token found. Please set MIURA_NEXUS_API_KEY environment variable "
            "or use 'miura-auth token set' to store a refresh token.",
            details={"sources_checked": ["environment_variable", "keyring"]},
        )

    def set_auth_api_key(self, api_key: str) -> None:
        """
        Store a refresh token (presented as API key to user) in storage backend.

        When storing in a specific backend, clears tokens from other backends
        to maintain a single source of truth.

        Args:
            api_key: The refresh token to store (user sees this as API key)

        Raises:
            ApiKeyInvalidError: If refresh token format is invalid
            KeyringError: If there's an error storing in keyring (when using keyring backend)
        """
        # Validate api key format first
        self.jwt_utils.validate_jwt_format(api_key)

        try:
            # Store in selected storage backend
            self.storage_backend.set("auth_api_key", api_key)

            # Clear tokens from other storage backends to maintain single source of truth
            from .storage import FileStorageBackend, KeyringStorageBackend, NoneStorageBackend

            current_backend_type = None
            if isinstance(self.storage_backend, FileStorageBackend):
                current_backend_type = "file"
            elif isinstance(self.storage_backend, KeyringStorageBackend):
                current_backend_type = "keyring"
            elif isinstance(self.storage_backend, NoneStorageBackend):
                current_backend_type = "none"

            # Clear from other backends
            if current_backend_type != "file":
                try:
                    file_backend = create_storage_backend("file")
                    if file_backend.exists("auth_api_key"):
                        file_backend.delete("auth_api_key")
                        logger.debug("Cleared token from file storage")
                except Exception as e:
                    logger.debug(f"Could not clear file storage: {e}")

            if current_backend_type != "keyring":
                try:
                    # Only create KeyringManager when needed for cleanup
                    if self._keyring_manager is None:
                        from .keyring import KeyringManager

                        self._keyring_manager = KeyringManager()
                    keyring_backend = create_storage_backend("keyring", self._keyring_manager)
                    if keyring_backend.exists("auth_api_key"):
                        keyring_backend.delete("auth_api_key")
                        logger.debug("Cleared token from keyring storage")
                except Exception as e:
                    logger.debug(f"Could not clear keyring storage: {e}")

            # Also update keyring cache if using keyring backend
            if isinstance(self.storage_backend, KeyringStorageBackend):
                cache = self._get_keyring_cache()
                cache["auth_api_key"] = api_key

            # Update in-memory cache and invalidate any cached access token if necessary
            self.token_manager.set_refresh_token(
                api_key,
                reason="Stored new refresh token, invalidating cached access token",
            )
        except KeyringError as e:
            logger.error(f"Storage failed: {e}")
            raise
        except Exception as e:
            logger.error(f"Unexpected error storing api key: {e}")
            raise NetworkError(
                f"Failed to store authentication api key: {e}",
                details={"error_type": type(e).__name__},
            )

    def clear_api_keys(self) -> None:
        """
        Clear all stored tokens from memory and storage backend.

        Raises:
            KeyringError: If there's an error clearing from keyring (when using keyring backend)
        """
        # Clear from storage backend
        try:
            self.storage_backend.delete("auth_api_key")
        except Exception as e:
            logger.warning(f"Unexpected error clearing api key from storage: {e}")
            # Don't raise here as we still want to clear from memory

        # Clear keyring cache
        self._clear_keyring_cache()

        # Clear from memory
        self.token_manager.clear_all("Clearing cached tokens from memory")

    def get_api_key_info(self) -> Dict[str, Any]:
        """
        Get information about the current refresh token and access token.

        Returns:
            dict: Token information including source (env var, keyring, etc.)
        """
        # Determine storage backend type
        from .storage import KeyringStorageBackend

        storage_type = None
        if isinstance(self.storage_backend, KeyringStorageBackend):
            storage_type = "keyring"
        self._sync_refresh_token_from_sources(storage_backend_type=storage_type)

        info = {
            "has_refresh_token": self.token_manager.refresh_token is not None,
            "has_access_token": self.token_manager.access_token is not None,
            "has_refresh_token_in_env": os.environ.get("MIURA_NEXUS_API_KEY") is not None,
        }

        # Check if refresh token exists in storage backend
        try:
            info["has_refresh_token_in_storage"] = self.storage_backend.exists("auth_api_key")
        except Exception:
            info["has_refresh_token_in_storage"] = False

        # Also check keyring for backward compatibility info (only if using keyring backend)
        if isinstance(self.storage_backend, KeyringStorageBackend):
            try:
                # KeyringManager is guaranteed to exist when using keyring backend
                assert self._keyring_manager is not None
                info["has_refresh_token_in_keyring"] = self._keyring_manager.has_password("auth_api_key")
            except Exception:
                info["has_refresh_token_in_keyring"] = False
        else:
            # Not using keyring backend, so no need to check keyring
            info["has_refresh_token_in_keyring"] = False

        return info

    def get_api_key_details(self) -> Dict[str, Any]:
        """
        Get detailed information about the current refresh token including JWT payload.

        Returns:
            dict: Detailed token information including JWT claims

        Raises:
            ApiKeyNotFoundError: If no refresh token is found
            ApiKeyInvalidError: If refresh token format is invalid
        """
        from .storage import FileStorageBackend, KeyringStorageBackend, NoneStorageBackend

        storage_type = None
        if isinstance(self.storage_backend, KeyringStorageBackend):
            storage_type = "keyring"
        elif isinstance(self.storage_backend, FileStorageBackend):
            storage_type = "file"
        elif isinstance(self.storage_backend, NoneStorageBackend):
            storage_type = "none"
        refresh_token = self._sync_refresh_token_from_sources(storage_backend_type=storage_type)

        if not refresh_token:
            raise ApiKeyNotFoundError(
                "No refresh token currently loaded",
                details={"operation": "get_api_key_details"},
            )

        try:
            # Always parse the refresh token payload for token details (what user stored)
            refresh_payload = self.jwt_utils.parse_jwt_payload(refresh_token)

            # Status/details must report the stored refresh token. Avoid a network refresh here:
            # resolving Keycloak config can load local .env values and override isolated storage tests.
            access_payload = None
            access_token_fetch_error = None
            if self.token_manager.access_token:
                try:
                    access_payload = self.jwt_utils.parse_jwt_payload(self.token_manager.access_token)
                except Exception as e:
                    access_token_fetch_error = f"Failed to parse cached access token: {e}"
                    logger.debug(access_token_fetch_error)

            # Get basic info
            basic_info = self.get_api_key_info()

            # Calculate expiration status based on REFRESH TOKEN (what user stored)
            import time

            current_time = time.time()
            refresh_token_exp = refresh_payload.get("exp")
            is_expired = False

            if refresh_token_exp:
                is_expired = current_time >= refresh_token_exp

            # Use access token payload for user info if available, otherwise use refresh token
            user_info_payload = access_payload if access_payload else refresh_payload

            # Add JWT-specific information
            # jwt_payload should be the refresh token payload (what user stored)
            details = {
                **basic_info,
                "jwt_payload": refresh_payload,  # Always use refresh token payload
                "is_expired": is_expired,
                "has_offline_access": self.jwt_utils.has_offline_access(refresh_payload),
                **self.jwt_utils.extract_user_info(user_info_payload),  # Use access token for user info if available
            }
            # Add access token fetch error if present
            if access_token_fetch_error:
                details["access_token_fetch_error"] = access_token_fetch_error
            return details

        except ApiKeyInvalidError:
            # If JWT parsing fails, return basic info without JWT details
            basic_info = self.get_api_key_info()
            basic_info["jwt_parse_error"] = "API key is not a valid JWT or is malformed"
            basic_info["is_expired"] = False  # Can't determine expiration if JWT is invalid
            basic_info["has_offline_access"] = False  # Can't determine scope if JWT is invalid
            return basic_info

    def close(self) -> None:
        """Close resources and clean up."""
        if self.keycloak_client:
            self.keycloak_client.close()

__init__

__init__(config_manager: Optional[ConfigurationManager] = None, storage_backend: Optional[str] = None, verify_ssl: Optional[bool] = None)

Initialize authentication handler.

Parameters:

Name Type Description Default
config_manager Optional[ConfigurationManager]

Optional ConfigurationManager instance

None
storage_backend Optional[str]

Storage backend type ("file", "keyring", or "none"). Defaults to "file".

None
verify_ssl Optional[bool]

Whether to verify SSL certificates. If None, reads from MIURA_KEYCLOAK_VERIFY_SSL env var (default: True)

None
Source code in src/miura/auth/handler.py
def __init__(
    self,
    config_manager: Optional[ConfigurationManager] = None,
    storage_backend: Optional[str] = None,
    verify_ssl: Optional[bool] = None,
):
    """
    Initialize authentication handler.

    Args:
        config_manager: Optional ConfigurationManager instance
        storage_backend: Storage backend type ("file", "keyring", or "none"). Defaults to "file".
        verify_ssl: Whether to verify SSL certificates. If None, reads from
            MIURA_KEYCLOAK_VERIFY_SSL env var (default: True)
    """
    # If verify_ssl not explicitly provided, read from environment variable
    if verify_ssl is None:
        verify_ssl = True
        verify_ssl_env = os.environ.get("MIURA_KEYCLOAK_VERIFY_SSL", "").lower()
        if verify_ssl_env in ("false", "0", "no", "off"):
            verify_ssl = False

    self._config_manager = config_manager
    self.token_manager = TokenManager()
    self._keyring_manager: Optional[Any] = None  # Lazy initialization - only create when needed
    self.jwt_utils = JWTUtils()
    self.keycloak_client = KeycloakClient(config_manager=config_manager, verify_ssl=verify_ssl)

    # Determine storage backend
    if storage_backend is None:
        # Default to file storage
        storage_backend = os.environ.get("MIURA_AUTH_STORAGE", "file")

    # Create storage backend
    if storage_backend == "keyring":
        # Only create KeyringManager when using keyring backend
        from .keyring import KeyringManager

        self._keyring_manager = KeyringManager()
        self.storage_backend = create_storage_backend("keyring", self._keyring_manager)
    elif storage_backend == "none":
        self.storage_backend = create_storage_backend("none")
    else:
        # Default to file - no need to create KeyringManager
        self.storage_backend = create_storage_backend("file")

    # Keyring cache (read once per command execution)
    self._keyring_cache: Optional[Dict[str, str]] = None
    self._keyring_cache_initialized = False

_get_keyring_cache

_get_keyring_cache() -> Dict[str, str]

Get keyring cache (read once per command execution).

Returns:

Type Description
Dict[str, str]

Dictionary of cached keyring values

Source code in src/miura/auth/handler.py
def _get_keyring_cache(self) -> Dict[str, str]:
    """
    Get keyring cache (read once per command execution).

    Returns:
        Dictionary of cached keyring values
    """
    if not self._keyring_cache_initialized:
        self._keyring_cache = {}
        # Only try to read from keyring if using keyring backend
        from .storage import KeyringStorageBackend

        if isinstance(self.storage_backend, KeyringStorageBackend):
            try:
                # KeyringManager is guaranteed to exist when using keyring backend
                assert self._keyring_manager is not None
                token = self._keyring_manager.get_password("auth_api_key")
                if token:
                    self._keyring_cache["auth_api_key"] = token
            except Exception:
                # Keyring not available or error - cache remains empty
                pass
        self._keyring_cache_initialized = True
    # After initialization, _keyring_cache is guaranteed to be a dict
    assert self._keyring_cache is not None
    return self._keyring_cache

_clear_keyring_cache

_clear_keyring_cache() -> None

Clear keyring cache (call on command completion).

Source code in src/miura/auth/handler.py
def _clear_keyring_cache(self) -> None:
    """Clear keyring cache (call on command completion)."""
    self._keyring_cache = None
    self._keyring_cache_initialized = False

_sync_refresh_token_from_sources

_sync_refresh_token_from_sources(storage_backend_type: Optional[str] = None) -> Optional[str]

Ensure the in-memory refresh token reflects the latest value from sources.

Resolution order: 1. Environment variable (MIURA_NEXUS_API_KEY) - highest priority 2. Storage backend (file, keyring, or none) - based on --storage flag 3. Keyring (if --storage=keyring or MIURA_AUTH_STORAGE=keyring)

Parameters:

Name Type Description Default
storage_backend_type Optional[str]

Optional override for storage backend type

None

Returns:

Type Description
Optional[str]

Optional[str]: The current refresh token if available, otherwise None.

Raises:

Type Description
ApiKeyInvalidError

If a token from any source is malformed.

KeyringError

If keyring access fails unexpectedly.

Source code in src/miura/auth/handler.py
def _sync_refresh_token_from_sources(self, storage_backend_type: Optional[str] = None) -> Optional[str]:
    """
    Ensure the in-memory refresh token reflects the latest value from sources.

    Resolution order:
    1. Environment variable (MIURA_NEXUS_API_KEY) - highest priority
    2. Storage backend (file, keyring, or none) - based on --storage flag
    3. Keyring (if --storage=keyring or MIURA_AUTH_STORAGE=keyring)

    Args:
        storage_backend_type: Optional override for storage backend type

    Returns:
        Optional[str]: The current refresh token if available, otherwise None.

    Raises:
        ApiKeyInvalidError: If a token from any source is malformed.
        KeyringError: If keyring access fails unexpectedly.
    """
    # Lazy import to avoid slow startup times
    from miura.nexus.infrastructure.config import ConfigurationManager

    try:
        # Trigger environment loading (if needed)
        ConfigurationManager(load_dotenv=False)
    except Exception as exc:
        logger.debug(f"Could not load application config during token sync: {exc}")

    current_token: Optional[str] = None
    token_source: Optional[str] = None

    # 1. Environment variable has highest priority
    env_refresh_token = os.environ.get("MIURA_NEXUS_API_KEY")
    if env_refresh_token:
        self.jwt_utils.validate_jwt_format(env_refresh_token)
        current_token = env_refresh_token
        token_source = "env"
    else:
        # 2. Try storage backend (file, keyring, or none)
        try:
            stored_token = self.storage_backend.get("auth_api_key")
            if stored_token:
                self.jwt_utils.validate_jwt_format(stored_token)
                current_token = stored_token
                token_source = "storage"
        except Exception as e:
            logger.debug(f"Storage backend access failed: {e}")

        # 3. If using keyring backend and storage didn't work, try keyring cache
        if not current_token and storage_backend_type == "keyring":
            try:
                cache = self._get_keyring_cache()
                cached_token = cache.get("auth_api_key")
                if cached_token:
                    self.jwt_utils.validate_jwt_format(cached_token)
                    current_token = cached_token
                    token_source = "keyring_cache"
            except Exception as e:
                logger.debug(f"Keyring cache access failed: {e}")

    if current_token is not None:
        refresh_changed = current_token != self.token_manager.refresh_token
        self.token_manager.set_refresh_token(
            current_token,
            reason="Refresh token changed, invalidating cached access token",
        )
        if refresh_changed:
            if token_source == "env":
                logger.debug("Using refresh token from environment variable (MIURA_NEXUS_API_KEY)")
            elif token_source == "storage":
                logger.debug(f"Using refresh token from {type(self.storage_backend).__name__} storage")
            elif token_source == "keyring_cache":
                logger.debug("Using refresh token from keyring cache")
        return self.token_manager.refresh_token

    # No token found in any source; clear cached tokens to avoid stale values
    if self.token_manager.has_tokens():
        logger.debug("No refresh token available, clearing cached tokens")
        self.token_manager.set_refresh_token(
            None,
            reason="Refresh token cleared; removing cached access token",
            force_invalidate=True,
        )
    else:
        self.token_manager.set_refresh_token(None)
    return None

get_auth_api_key

get_auth_api_key() -> str

Get an access token for API authentication. Uses refresh token to fetch fresh access tokens transparently. Priority order: 1) Environment variable, 2) Memory cache, 3) Keyring.

Returns:

Name Type Description
str str

The access token for API authentication

Raises:

Type Description
ApiKeyNotFoundError

If no refresh token is found in any source

KeyringError

If there's an error accessing keyring

ApiKeyInvalidError

If refresh token format is invalid

PermissionError

If token refresh fails

Source code in src/miura/auth/handler.py
def get_auth_api_key(self) -> str:
    """
    Get an access token for API authentication.
    Uses refresh token to fetch fresh access tokens transparently.
    Priority order: 1) Environment variable, 2) Memory cache, 3) Keyring.

    Returns:
        str: The access token for API authentication

    Raises:
        ApiKeyNotFoundError: If no refresh token is found in any source
        KeyringError: If there's an error accessing keyring
        ApiKeyInvalidError: If refresh token format is invalid
        PermissionError: If token refresh fails
    """
    # Ensure cached refresh token reflects latest source state
    # Determine storage backend type from handler
    from .storage import KeyringStorageBackend, FileStorageBackend, NoneStorageBackend

    storage_type = None
    if isinstance(self.storage_backend, KeyringStorageBackend):
        storage_type = "keyring"
    elif isinstance(self.storage_backend, FileStorageBackend):
        storage_type = "file"
    elif isinstance(self.storage_backend, NoneStorageBackend):
        storage_type = "none"
    refresh_token = self._sync_refresh_token_from_sources(storage_backend_type=storage_type)

    # Check if we have a valid access token in memory first
    if self.token_manager.access_token:
        # Check if the access token has expired
        try:
            if not self.jwt_utils.is_expired(self.token_manager.access_token):
                logger.debug("Using cached access token")
                return self.token_manager.access_token
            else:
                logger.debug("Access token expired or expiring soon, refreshing...")
        except Exception as e:
            logger.debug(f"Could not check access token expiration: {e}, refreshing...")

        # If we reach here, the token is expired or we couldn't check it
        self.token_manager.invalidate_access_token("Access token expired or could not be verified; clearing cache")
    else:
        logger.debug("Access token cache empty; fetching fresh token")

    # Now use the refresh token to get an access token
    if refresh_token:
        access_token = self.keycloak_client.refresh_access_token(refresh_token)
        self.token_manager.set_access_token(access_token)
        return access_token

    # Determine which error to surface for missing refresh token
    if os.environ.get("MIURA_NEXUS_API_KEY"):
        raise ApiKeyInvalidError(
            "MIURA_NEXUS_API_KEY is set but could not be validated",
            details={"source": "environment_variable"},
        )

    # Note: Keyring availability is checked lazily when operations are attempted
    # If keyring operations fail, appropriate errors will be raised

    raise ApiKeyNotFoundError(
        "No refresh token found. Please set MIURA_NEXUS_API_KEY environment variable "
        "or use 'miura-auth token set' to store a refresh token.",
        details={"sources_checked": ["environment_variable", "keyring"]},
    )

set_auth_api_key

set_auth_api_key(api_key: str) -> None

Store a refresh token (presented as API key to user) in storage backend.

When storing in a specific backend, clears tokens from other backends to maintain a single source of truth.

Parameters:

Name Type Description Default
api_key str

The refresh token to store (user sees this as API key)

required

Raises:

Type Description
ApiKeyInvalidError

If refresh token format is invalid

KeyringError

If there's an error storing in keyring (when using keyring backend)

Source code in src/miura/auth/handler.py
def set_auth_api_key(self, api_key: str) -> None:
    """
    Store a refresh token (presented as API key to user) in storage backend.

    When storing in a specific backend, clears tokens from other backends
    to maintain a single source of truth.

    Args:
        api_key: The refresh token to store (user sees this as API key)

    Raises:
        ApiKeyInvalidError: If refresh token format is invalid
        KeyringError: If there's an error storing in keyring (when using keyring backend)
    """
    # Validate api key format first
    self.jwt_utils.validate_jwt_format(api_key)

    try:
        # Store in selected storage backend
        self.storage_backend.set("auth_api_key", api_key)

        # Clear tokens from other storage backends to maintain single source of truth
        from .storage import FileStorageBackend, KeyringStorageBackend, NoneStorageBackend

        current_backend_type = None
        if isinstance(self.storage_backend, FileStorageBackend):
            current_backend_type = "file"
        elif isinstance(self.storage_backend, KeyringStorageBackend):
            current_backend_type = "keyring"
        elif isinstance(self.storage_backend, NoneStorageBackend):
            current_backend_type = "none"

        # Clear from other backends
        if current_backend_type != "file":
            try:
                file_backend = create_storage_backend("file")
                if file_backend.exists("auth_api_key"):
                    file_backend.delete("auth_api_key")
                    logger.debug("Cleared token from file storage")
            except Exception as e:
                logger.debug(f"Could not clear file storage: {e}")

        if current_backend_type != "keyring":
            try:
                # Only create KeyringManager when needed for cleanup
                if self._keyring_manager is None:
                    from .keyring import KeyringManager

                    self._keyring_manager = KeyringManager()
                keyring_backend = create_storage_backend("keyring", self._keyring_manager)
                if keyring_backend.exists("auth_api_key"):
                    keyring_backend.delete("auth_api_key")
                    logger.debug("Cleared token from keyring storage")
            except Exception as e:
                logger.debug(f"Could not clear keyring storage: {e}")

        # Also update keyring cache if using keyring backend
        if isinstance(self.storage_backend, KeyringStorageBackend):
            cache = self._get_keyring_cache()
            cache["auth_api_key"] = api_key

        # Update in-memory cache and invalidate any cached access token if necessary
        self.token_manager.set_refresh_token(
            api_key,
            reason="Stored new refresh token, invalidating cached access token",
        )
    except KeyringError as e:
        logger.error(f"Storage failed: {e}")
        raise
    except Exception as e:
        logger.error(f"Unexpected error storing api key: {e}")
        raise NetworkError(
            f"Failed to store authentication api key: {e}",
            details={"error_type": type(e).__name__},
        )

clear_api_keys

clear_api_keys() -> None

Clear all stored tokens from memory and storage backend.

Raises:

Type Description
KeyringError

If there's an error clearing from keyring (when using keyring backend)

Source code in src/miura/auth/handler.py
def clear_api_keys(self) -> None:
    """
    Clear all stored tokens from memory and storage backend.

    Raises:
        KeyringError: If there's an error clearing from keyring (when using keyring backend)
    """
    # Clear from storage backend
    try:
        self.storage_backend.delete("auth_api_key")
    except Exception as e:
        logger.warning(f"Unexpected error clearing api key from storage: {e}")
        # Don't raise here as we still want to clear from memory

    # Clear keyring cache
    self._clear_keyring_cache()

    # Clear from memory
    self.token_manager.clear_all("Clearing cached tokens from memory")

get_api_key_info

get_api_key_info() -> Dict[str, Any]

Get information about the current refresh token and access token.

Returns:

Name Type Description
dict Dict[str, Any]

Token information including source (env var, keyring, etc.)

Source code in src/miura/auth/handler.py
def get_api_key_info(self) -> Dict[str, Any]:
    """
    Get information about the current refresh token and access token.

    Returns:
        dict: Token information including source (env var, keyring, etc.)
    """
    # Determine storage backend type
    from .storage import KeyringStorageBackend

    storage_type = None
    if isinstance(self.storage_backend, KeyringStorageBackend):
        storage_type = "keyring"
    self._sync_refresh_token_from_sources(storage_backend_type=storage_type)

    info = {
        "has_refresh_token": self.token_manager.refresh_token is not None,
        "has_access_token": self.token_manager.access_token is not None,
        "has_refresh_token_in_env": os.environ.get("MIURA_NEXUS_API_KEY") is not None,
    }

    # Check if refresh token exists in storage backend
    try:
        info["has_refresh_token_in_storage"] = self.storage_backend.exists("auth_api_key")
    except Exception:
        info["has_refresh_token_in_storage"] = False

    # Also check keyring for backward compatibility info (only if using keyring backend)
    if isinstance(self.storage_backend, KeyringStorageBackend):
        try:
            # KeyringManager is guaranteed to exist when using keyring backend
            assert self._keyring_manager is not None
            info["has_refresh_token_in_keyring"] = self._keyring_manager.has_password("auth_api_key")
        except Exception:
            info["has_refresh_token_in_keyring"] = False
    else:
        # Not using keyring backend, so no need to check keyring
        info["has_refresh_token_in_keyring"] = False

    return info

get_api_key_details

get_api_key_details() -> Dict[str, Any]

Get detailed information about the current refresh token including JWT payload.

Returns:

Name Type Description
dict Dict[str, Any]

Detailed token information including JWT claims

Raises:

Type Description
ApiKeyNotFoundError

If no refresh token is found

ApiKeyInvalidError

If refresh token format is invalid

Source code in src/miura/auth/handler.py
def get_api_key_details(self) -> Dict[str, Any]:
    """
    Get detailed information about the current refresh token including JWT payload.

    Returns:
        dict: Detailed token information including JWT claims

    Raises:
        ApiKeyNotFoundError: If no refresh token is found
        ApiKeyInvalidError: If refresh token format is invalid
    """
    from .storage import FileStorageBackend, KeyringStorageBackend, NoneStorageBackend

    storage_type = None
    if isinstance(self.storage_backend, KeyringStorageBackend):
        storage_type = "keyring"
    elif isinstance(self.storage_backend, FileStorageBackend):
        storage_type = "file"
    elif isinstance(self.storage_backend, NoneStorageBackend):
        storage_type = "none"
    refresh_token = self._sync_refresh_token_from_sources(storage_backend_type=storage_type)

    if not refresh_token:
        raise ApiKeyNotFoundError(
            "No refresh token currently loaded",
            details={"operation": "get_api_key_details"},
        )

    try:
        # Always parse the refresh token payload for token details (what user stored)
        refresh_payload = self.jwt_utils.parse_jwt_payload(refresh_token)

        # Status/details must report the stored refresh token. Avoid a network refresh here:
        # resolving Keycloak config can load local .env values and override isolated storage tests.
        access_payload = None
        access_token_fetch_error = None
        if self.token_manager.access_token:
            try:
                access_payload = self.jwt_utils.parse_jwt_payload(self.token_manager.access_token)
            except Exception as e:
                access_token_fetch_error = f"Failed to parse cached access token: {e}"
                logger.debug(access_token_fetch_error)

        # Get basic info
        basic_info = self.get_api_key_info()

        # Calculate expiration status based on REFRESH TOKEN (what user stored)
        import time

        current_time = time.time()
        refresh_token_exp = refresh_payload.get("exp")
        is_expired = False

        if refresh_token_exp:
            is_expired = current_time >= refresh_token_exp

        # Use access token payload for user info if available, otherwise use refresh token
        user_info_payload = access_payload if access_payload else refresh_payload

        # Add JWT-specific information
        # jwt_payload should be the refresh token payload (what user stored)
        details = {
            **basic_info,
            "jwt_payload": refresh_payload,  # Always use refresh token payload
            "is_expired": is_expired,
            "has_offline_access": self.jwt_utils.has_offline_access(refresh_payload),
            **self.jwt_utils.extract_user_info(user_info_payload),  # Use access token for user info if available
        }
        # Add access token fetch error if present
        if access_token_fetch_error:
            details["access_token_fetch_error"] = access_token_fetch_error
        return details

    except ApiKeyInvalidError:
        # If JWT parsing fails, return basic info without JWT details
        basic_info = self.get_api_key_info()
        basic_info["jwt_parse_error"] = "API key is not a valid JWT or is malformed"
        basic_info["is_expired"] = False  # Can't determine expiration if JWT is invalid
        basic_info["has_offline_access"] = False  # Can't determine scope if JWT is invalid
        return basic_info

close

close() -> None

Close resources and clean up.

Source code in src/miura/auth/handler.py
def close(self) -> None:
    """Close resources and clean up."""
    if self.keycloak_client:
        self.keycloak_client.close()

JWTUtils

Utilities for JWT token handling.

Source code in src/miura/auth/jwt.py
class JWTUtils:
    """Utilities for JWT token handling."""

    @staticmethod
    def parse_jwt_payload(api_key: str) -> Dict[str, Any]:
        """
        Parse a JWT api key and extract its payload.

        Args:
            api_key: JWT api key string

        Returns:
            dict: API key payload

        Raises:
            ApiKeyInvalidError: If api key is malformed
        """
        try:
            # JWT api_keys have 3 parts separated by dots
            parts = api_key.split(".")
            if len(parts) != 3:
                raise ApiKeyInvalidError(
                    "Invalid JWT api key format. Expected 3 parts separated by dots.",
                    details={"parts_count": len(parts)},
                )

            # Decode the payload (second part)
            payload_part = parts[1]
            # Add padding if needed
            padding = 4 - (len(payload_part) % 4)
            if padding != 4:
                payload_part += "=" * padding

            payload_bytes = base64.urlsafe_b64decode(payload_part)
            payload = json.loads(payload_bytes.decode("utf-8"))

            return payload
        except (ValueError, json.JSONDecodeError) as e:
            raise ApiKeyInvalidError(
                f"Failed to parse JWT api key: {e}",
                details={"error_type": type(e).__name__},
            )

    @staticmethod
    def validate_jwt_format(api_key: str) -> None:
        """
        Validate that an api key has the basic JWT format.

        Args:
            api_key: API key string to validate

        Raises:
            ApiKeyInvalidError: If api key format is invalid
        """
        if not api_key or not isinstance(api_key, str):
            raise ApiKeyInvalidError(
                "API key must be a non-empty string",
                details={"api_key_type": type(api_key).__name__},
            )

        # Basic JWT format check
        parts = api_key.split(".")
        if len(parts) != 3:
            raise ApiKeyInvalidError(
                "API key does not appear to be a valid JWT. Expected 3 parts separated by dots.",
                details={"parts_count": len(parts)},
            )

        # Check that each part is base64-like
        for i, part in enumerate(parts):
            if not part:
                raise ApiKeyInvalidError(
                    f"JWT part {i + 1} is empty",
                    details={"part_index": i + 1, "total_parts": len(parts)},
                )
            # Basic base64 character check
            if not all(c.isalnum() or c in "-_" for c in part):
                raise ApiKeyInvalidError(
                    f"JWT part {i + 1} contains invalid characters",
                    details={"part_index": i + 1, "part_length": len(part)},
                )

    @staticmethod
    def get_expiration_time(api_key: str) -> Optional[float]:
        """
        Extract expiration time from JWT api key.

        Args:
            api_key: JWT api key string

        Returns:
            float: Expiration timestamp, or None if not found
        """
        try:
            payload = JWTUtils.parse_jwt_payload(api_key)
            exp = payload.get("exp")
            if exp:
                return float(exp)
            return None
        except ApiKeyInvalidError:
            return None

    @staticmethod
    def is_expired(api_key: str, buffer_seconds: int = 300) -> bool:
        """
        Check if a JWT token is expired or will expire soon.

        Args:
            api_key: JWT api key string
            buffer_seconds: Buffer time in seconds before actual expiration

        Returns:
            bool: True if expired or expiring soon, False otherwise
        """
        try:
            expiration_time = JWTUtils.get_expiration_time(api_key)
            if expiration_time and expiration_time > 0:
                current_time = time.time()
                return current_time >= (expiration_time - buffer_seconds)
            return True  # If we can't determine expiration, assume it's expired
        except Exception:
            return True  # If we can't check expiration, assume it's expired

    @staticmethod
    def extract_user_info(payload: Dict[str, Any]) -> Dict[str, Any]:
        """
        Extract user information from JWT payload.

        Args:
            payload: JWT payload dictionary

        Returns:
            dict: User information extracted from payload
        """
        return {
            "name": payload.get("name"),
            "email": payload.get("email"),
            "preferred_username": payload.get("preferred_username"),
            "given_name": payload.get("given_name"),
            "family_name": payload.get("family_name"),
        }

    @staticmethod
    def has_offline_access(payload: Dict[str, Any]) -> bool:
        """
        Check if JWT payload has offline access scope.

        Args:
            payload: JWT payload dictionary

        Returns:
            bool: True if offline_access scope is present
        """
        scope = payload.get("scope", "")
        return "offline_access" in scope

parse_jwt_payload staticmethod

parse_jwt_payload(api_key: str) -> Dict[str, Any]

Parse a JWT api key and extract its payload.

Parameters:

Name Type Description Default
api_key str

JWT api key string

required

Returns:

Name Type Description
dict Dict[str, Any]

API key payload

Raises:

Type Description
ApiKeyInvalidError

If api key is malformed

Source code in src/miura/auth/jwt.py
@staticmethod
def parse_jwt_payload(api_key: str) -> Dict[str, Any]:
    """
    Parse a JWT api key and extract its payload.

    Args:
        api_key: JWT api key string

    Returns:
        dict: API key payload

    Raises:
        ApiKeyInvalidError: If api key is malformed
    """
    try:
        # JWT api_keys have 3 parts separated by dots
        parts = api_key.split(".")
        if len(parts) != 3:
            raise ApiKeyInvalidError(
                "Invalid JWT api key format. Expected 3 parts separated by dots.",
                details={"parts_count": len(parts)},
            )

        # Decode the payload (second part)
        payload_part = parts[1]
        # Add padding if needed
        padding = 4 - (len(payload_part) % 4)
        if padding != 4:
            payload_part += "=" * padding

        payload_bytes = base64.urlsafe_b64decode(payload_part)
        payload = json.loads(payload_bytes.decode("utf-8"))

        return payload
    except (ValueError, json.JSONDecodeError) as e:
        raise ApiKeyInvalidError(
            f"Failed to parse JWT api key: {e}",
            details={"error_type": type(e).__name__},
        )

validate_jwt_format staticmethod

validate_jwt_format(api_key: str) -> None

Validate that an api key has the basic JWT format.

Parameters:

Name Type Description Default
api_key str

API key string to validate

required

Raises:

Type Description
ApiKeyInvalidError

If api key format is invalid

Source code in src/miura/auth/jwt.py
@staticmethod
def validate_jwt_format(api_key: str) -> None:
    """
    Validate that an api key has the basic JWT format.

    Args:
        api_key: API key string to validate

    Raises:
        ApiKeyInvalidError: If api key format is invalid
    """
    if not api_key or not isinstance(api_key, str):
        raise ApiKeyInvalidError(
            "API key must be a non-empty string",
            details={"api_key_type": type(api_key).__name__},
        )

    # Basic JWT format check
    parts = api_key.split(".")
    if len(parts) != 3:
        raise ApiKeyInvalidError(
            "API key does not appear to be a valid JWT. Expected 3 parts separated by dots.",
            details={"parts_count": len(parts)},
        )

    # Check that each part is base64-like
    for i, part in enumerate(parts):
        if not part:
            raise ApiKeyInvalidError(
                f"JWT part {i + 1} is empty",
                details={"part_index": i + 1, "total_parts": len(parts)},
            )
        # Basic base64 character check
        if not all(c.isalnum() or c in "-_" for c in part):
            raise ApiKeyInvalidError(
                f"JWT part {i + 1} contains invalid characters",
                details={"part_index": i + 1, "part_length": len(part)},
            )

get_expiration_time staticmethod

get_expiration_time(api_key: str) -> Optional[float]

Extract expiration time from JWT api key.

Parameters:

Name Type Description Default
api_key str

JWT api key string

required

Returns:

Name Type Description
float Optional[float]

Expiration timestamp, or None if not found

Source code in src/miura/auth/jwt.py
@staticmethod
def get_expiration_time(api_key: str) -> Optional[float]:
    """
    Extract expiration time from JWT api key.

    Args:
        api_key: JWT api key string

    Returns:
        float: Expiration timestamp, or None if not found
    """
    try:
        payload = JWTUtils.parse_jwt_payload(api_key)
        exp = payload.get("exp")
        if exp:
            return float(exp)
        return None
    except ApiKeyInvalidError:
        return None

is_expired staticmethod

is_expired(api_key: str, buffer_seconds: int = 300) -> bool

Check if a JWT token is expired or will expire soon.

Parameters:

Name Type Description Default
api_key str

JWT api key string

required
buffer_seconds int

Buffer time in seconds before actual expiration

300

Returns:

Name Type Description
bool bool

True if expired or expiring soon, False otherwise

Source code in src/miura/auth/jwt.py
@staticmethod
def is_expired(api_key: str, buffer_seconds: int = 300) -> bool:
    """
    Check if a JWT token is expired or will expire soon.

    Args:
        api_key: JWT api key string
        buffer_seconds: Buffer time in seconds before actual expiration

    Returns:
        bool: True if expired or expiring soon, False otherwise
    """
    try:
        expiration_time = JWTUtils.get_expiration_time(api_key)
        if expiration_time and expiration_time > 0:
            current_time = time.time()
            return current_time >= (expiration_time - buffer_seconds)
        return True  # If we can't determine expiration, assume it's expired
    except Exception:
        return True  # If we can't check expiration, assume it's expired

extract_user_info staticmethod

extract_user_info(payload: Dict[str, Any]) -> Dict[str, Any]

Extract user information from JWT payload.

Parameters:

Name Type Description Default
payload Dict[str, Any]

JWT payload dictionary

required

Returns:

Name Type Description
dict Dict[str, Any]

User information extracted from payload

Source code in src/miura/auth/jwt.py
@staticmethod
def extract_user_info(payload: Dict[str, Any]) -> Dict[str, Any]:
    """
    Extract user information from JWT payload.

    Args:
        payload: JWT payload dictionary

    Returns:
        dict: User information extracted from payload
    """
    return {
        "name": payload.get("name"),
        "email": payload.get("email"),
        "preferred_username": payload.get("preferred_username"),
        "given_name": payload.get("given_name"),
        "family_name": payload.get("family_name"),
    }

has_offline_access staticmethod

has_offline_access(payload: Dict[str, Any]) -> bool

Check if JWT payload has offline access scope.

Parameters:

Name Type Description Default
payload Dict[str, Any]

JWT payload dictionary

required

Returns:

Name Type Description
bool bool

True if offline_access scope is present

Source code in src/miura/auth/jwt.py
@staticmethod
def has_offline_access(payload: Dict[str, Any]) -> bool:
    """
    Check if JWT payload has offline access scope.

    Args:
        payload: JWT payload dictionary

    Returns:
        bool: True if offline_access scope is present
    """
    scope = payload.get("scope", "")
    return "offline_access" in scope

KeycloakClient

Client for Keycloak token operations with retry logic.

Source code in src/miura/auth/keycloak.py
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
class KeycloakClient:
    """Client for Keycloak token operations with retry logic."""

    def __init__(
        self,
        config_manager: Optional[ConfigurationManager] = None,
        retry_policy: Optional[RetryPolicy] = None,
        verify_ssl: bool = True,
    ) -> None:
        """
        Initialize Keycloak client.

        Args:
            config_manager: Optional ConfigurationManager instance
            retry_policy: Optional RetryPolicy (uses default from config if not provided)
            verify_ssl: Whether to verify SSL certificates (default: True)
        """
        self._config_manager = config_manager
        self._config: Optional[Dict[str, Any]] = None
        self._retry_policy = retry_policy
        self._verify_ssl = verify_ssl
        self._http_client: Optional["httpx.Client"] = None  # httpx.Client

    def _get_http_client(self):
        """Get or create httpx client with connection pooling."""
        if self._http_client is None:
            import httpx

            # Get connection limits from config
            if self._config_manager:
                internal_config = self._config_manager.internal
                connection_limits = internal_config.http.default_connection_limits
            else:
                from miura.nexus.infrastructure.config import ConfigurationManager

                config_manager = ConfigurationManager(load_dotenv=False)
                internal_config = config_manager.internal
                connection_limits = internal_config.http.default_connection_limits

            # Create httpx client with connection pooling
            # Note: httpx automatically respects HTTP_PROXY, HTTPS_PROXY, NO_PROXY env vars
            # so we don't need to pass proxies explicitly
            self._http_client = httpx.Client(
                limits=httpx.Limits(
                    max_connections=connection_limits.total,
                    max_keepalive_connections=connection_limits.per_host,
                ),
                timeout=httpx.Timeout(
                    connect=10.0,
                    read=30.0,
                    write=30.0,
                    pool=30.0,
                ),
                headers={"User-Agent": _get_user_agent()},
                verify=self._verify_ssl,
            )
        return self._http_client

    def _get_retry_policy(self) -> RetryPolicy:
        """Get retry policy from config or use default."""
        if self._retry_policy is not None:
            return self._retry_policy

        # Get from config manager if available
        if self._config_manager:
            # Check if there's a retry policy in config
            # For now, use default RetryPolicy
            return RetryPolicy()

        return RetryPolicy()

    def _get_config(self) -> Dict[str, Any]:
        """
        Resolve the effective Keycloak configuration from InternalConfig.

        Returns:
            Dict[str, Any]: Mapping with keys base_url, realm, client_id, timeout_connect, timeout_read.

        Raises:
            ValidationError: If configuration is invalid
        """
        if self._config is not None:
            return self._config

        # Get config manager or create default
        if self._config_manager is None:
            from miura.nexus.infrastructure.config import ConfigurationManager

            self._config_manager = ConfigurationManager(load_dotenv=True)

        # Get Keycloak config from InternalConfig
        internal_config = self._config_manager.internal
        auth_config = internal_config.auth
        http_config = internal_config.http

        # Validate configuration
        if not auth_config.keycloak_base_url:
            raise ValidationError(
                "Keycloak base URL is not configured. Set MIURA_INTERNAL_KEYCLOAK_BASE_URL or use default.",
                details={"config_source": "InternalConfig.auth.keycloak_base_url"},
            )

        if not auth_config.keycloak_realm:
            raise ValidationError(
                "Keycloak realm is not configured. Set MIURA_INTERNAL_KEYCLOAK_REALM or use default.",
                details={"config_source": "InternalConfig.auth.keycloak_realm"},
            )

        if not auth_config.keycloak_client_id:
            raise ValidationError(
                "Keycloak client ID is not configured. Set MIURA_INTERNAL_KEYCLOAK_CLIENT_ID or use default.",
                details={"config_source": "InternalConfig.auth.keycloak_client_id"},
            )

        # Validate URL format
        base_url = auth_config.keycloak_base_url
        if not base_url.startswith(("http://", "https://")):
            raise ValidationError(
                f"Invalid Keycloak base URL format: {base_url}. Must start with http:// or https://",
                details={"base_url": base_url, "config_source": "InternalConfig.auth.keycloak_base_url"},
            )

        # Get timeout from shared Timeouts config
        timeouts = http_config.default_timeouts
        timeout_connect = timeouts.connect if timeouts else 10.0
        timeout_read = timeouts.read if timeouts else 30.0

        self._config = {
            "base_url": base_url,
            "realm": auth_config.keycloak_realm,
            "client_id": auth_config.keycloak_client_id,
            "timeout_connect": timeout_connect,
            "timeout_read": timeout_read,
        }
        return self._config

    def _map_httpx_error(self, error: Exception, endpoint: str) -> Exception:
        """
        Map httpx exceptions to shared exception hierarchy.

        Args:
            error: httpx exception
            endpoint: Endpoint URL for context

        Returns:
            Appropriate NexusError subclass
        """
        import httpx

        if isinstance(error, httpx.TimeoutException):
            kc_config = self._get_config()
            timeout = kc_config.get("timeout_read", 30.0)
            return TimeoutError("refresh_access_token", timeout, details={"endpoint": endpoint})
        elif isinstance(error, httpx.ConnectError):
            return NetworkError(
                f"Failed to connect to Keycloak: {error}",
                details={"endpoint": endpoint, "error_type": type(error).__name__},
            )
        elif isinstance(error, httpx.HTTPStatusError):
            status_code = error.response.status_code if error.response else None

            # Try to extract Keycloak error details from response body
            keycloak_error = None
            keycloak_error_description = None
            if error.response:
                try:
                    error_data = error.response.json()
                    keycloak_error = error_data.get("error")
                    keycloak_error_description = error_data.get("error_description")
                except (ValueError, KeyError, AttributeError):
                    # Response is not JSON or doesn't have expected structure
                    pass

            if status_code == 401:
                error_msg = "Authentication failed. Token may be expired or invalid."
                if keycloak_error_description:
                    error_msg = f"Authentication failed: {keycloak_error_description}"
                return PermissionError(
                    error_msg,
                    details={
                        "endpoint": endpoint,
                        "status_code": status_code,
                        "keycloak_error": keycloak_error,
                        "keycloak_error_description": keycloak_error_description,
                    },
                )
            elif status_code == 400:
                # 400 errors are typically client errors (invalid request, invalid token, etc.)
                # If we have error details, include them
                if keycloak_error and keycloak_error_description:
                    error_msg = f"Keycloak error ({keycloak_error}): {keycloak_error_description}"
                elif keycloak_error:
                    error_msg = f"Keycloak error: {keycloak_error}"
                else:
                    error_msg = f"Bad request to Keycloak: {error}"
                return ApiKeyInvalidError(
                    error_msg,
                    details={
                        "endpoint": endpoint,
                        "status_code": status_code,
                        "keycloak_error": keycloak_error,
                        "keycloak_error_description": keycloak_error_description,
                    },
                )
            elif status_code == 429:
                # Rate limit - retryable
                error_msg = "Rate limited by Keycloak"
                if keycloak_error_description:
                    error_msg = f"Rate limited by Keycloak: {keycloak_error_description}"
                return NetworkError(
                    error_msg,
                    details={"endpoint": endpoint, "status_code": status_code},
                )
            elif status_code and status_code >= 500:
                # Backend/service error (not network) - use safe user-facing message
                from miura.sdk.exceptions import ServiceError

                reason = "The authentication service encountered an error. Please try again later."
                details_ = {"endpoint": endpoint, "status_code": status_code}
                if keycloak_error_description:
                    details_["_raw_message"] = keycloak_error_description
                return ServiceError(reason, details_)
            else:
                error_msg = f"HTTP error from Keycloak: {error}"
                if keycloak_error_description:
                    error_msg = f"Keycloak error: {keycloak_error_description}"
                return NetworkError(
                    error_msg,
                    details={
                        "endpoint": endpoint,
                        "status_code": status_code,
                        "keycloak_error": keycloak_error,
                        "keycloak_error_description": keycloak_error_description,
                    },
                )
        elif isinstance(error, httpx.RequestError):
            return NetworkError(
                f"Request error: {error}",
                details={"endpoint": endpoint, "error_type": type(error).__name__},
            )
        else:
            return NetworkError(
                f"Unexpected error: {error}",
                details={"endpoint": endpoint, "error_type": type(error).__name__},
            )

    def refresh_access_token(self, refresh_token: str) -> str:
        """
        Use refresh token to get a fresh access token from Keycloak.

        Implements retry logic using shared RetryPolicy.

        Args:
            refresh_token: The refresh token to use

        Returns:
            str: Fresh access token

        Raises:
            PermissionError: If authentication fails (401)
            NetworkError: If network request fails
            TimeoutError: If request times out
            ApiKeyInvalidError: If token is invalid
        """
        import httpx

        # Get configuration
        kc_config = self._get_config()
        kc_base_url = kc_config["base_url"]
        kc_realm = kc_config["realm"]
        kc_client_id = kc_config["client_id"]
        timeout_connect = kc_config["timeout_connect"]
        timeout_read = kc_config["timeout_read"]

        token_url = f"{kc_base_url}/realms/{kc_realm}/protocol/openid-connect/token"

        # Prepare the request
        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        data = {
            "grant_type": "refresh_token",
            "client_id": kc_client_id,
            "refresh_token": refresh_token,
        }

        logger.debug(f"Refreshing access token using Keycloak endpoint: {token_url}")

        # Get retry policy
        retry_policy = self._get_retry_policy()

        # Get HTTP client
        http_client = self._get_http_client()

        # Retry loop using shared RetryPolicy
        last_exception: Optional[Exception] = None
        for attempt in range(retry_policy.max_retries + 1):
            try:
                # Make the request with timeout
                # httpx.Timeout requires either a default or all four parameters (connect, read, write, pool)
                # Use a default timeout value (total timeout) which applies to all operations
                total_timeout = timeout_connect + timeout_read
                timeout = httpx.Timeout(total_timeout)
                response = http_client.post(
                    token_url,
                    headers=headers,
                    data=data,
                    timeout=timeout,
                )
                response.raise_for_status()

                # Parse the response
                token_data = response.json()
                access_token = token_data.get("access_token")

                if not access_token:
                    raise ApiKeyInvalidError(
                        "No access token received from Keycloak",
                        details={"endpoint": token_url},
                    )

                logger.debug("Successfully refreshed access token")
                return access_token

            except httpx.HTTPStatusError as e:
                # Check if it's a 401 (auth error - don't retry)
                if e.response and e.response.status_code == 401:
                    raise self._map_httpx_error(e, token_url)
                # Check if it's a 400 with invalid_token (token validation error - don't retry)
                if e.response and e.response.status_code == 400:
                    try:
                        error_data = e.response.json()
                        error_code = error_data.get("error", "")
                        error_description = error_data.get("error_description", "")

                        # Keycloak returns "invalid_token" for invalid refresh tokens
                        if error_code == "invalid_token":
                            default_msg = "The refresh token is expired, revoked, or malformed."
                            error_msg = f"Invalid refresh token. {error_description or default_msg}"
                            raise ApiKeyInvalidError(
                                error_msg,
                                details={
                                    "endpoint": token_url,
                                    "status_code": 400,
                                    "keycloak_error": error_code,
                                    "keycloak_error_description": error_description,
                                },
                            )
                    except (ValueError, KeyError, AttributeError):
                        # If we can't parse the response, fall through to generic handling
                        pass
                # Other HTTP errors might be retryable
                last_exception = e
            except (httpx.TimeoutException, httpx.ConnectError, httpx.RequestError) as e:
                # Network/timeout errors - retryable
                last_exception = e
            except ApiKeyInvalidError:
                # Don't retry invalid token errors - re-raise immediately
                raise
            except (KeyError, ValueError) as e:
                # Invalid response - don't retry
                logger.error(f"Invalid response from Keycloak: {e}")
                raise ApiKeyInvalidError(
                    f"Invalid response from Keycloak: {e}",
                    details={"endpoint": token_url},
                )
            except Exception as e:
                # Unexpected errors - check if retryable
                last_exception = e

            # Check if we should retry
            # Map httpx exceptions to NetworkError/TimeoutError for retry check
            retry_exception = last_exception
            if isinstance(last_exception, (httpx.TimeoutException, httpx.ConnectError, httpx.RequestError)):
                # These are network errors - check retry using a NetworkError
                from miura.sdk.exceptions import NetworkError, TimeoutError

                if isinstance(last_exception, httpx.TimeoutException):
                    retry_exception = TimeoutError("refresh_access_token", 30.0, details={})
                else:
                    retry_exception = NetworkError(str(last_exception), details={})

            if last_exception and retry_policy.should_retry(retry_exception, attempt):
                # Extract Retry-After header if present
                retry_after = None
                if isinstance(last_exception, httpx.HTTPStatusError) and last_exception.response:
                    retry_after_header = last_exception.response.headers.get("Retry-After")
                    if retry_after_header:
                        try:
                            retry_after = float(retry_after_header)
                        except (ValueError, TypeError):
                            pass

                # Calculate backoff using shared RetryPolicy
                backoff = retry_policy.calculate_backoff(attempt, retry_after)

                # Wait before retry
                time.sleep(backoff)
            else:
                # Don't retry - map error and raise
                if last_exception:
                    if isinstance(last_exception, httpx.HTTPStatusError):
                        raise self._map_httpx_error(last_exception, token_url)
                    elif isinstance(last_exception, (httpx.TimeoutException, httpx.ConnectError, httpx.RequestError)):
                        raise self._map_httpx_error(last_exception, token_url)
                    else:
                        raise NetworkError(
                            f"Failed to refresh access token: {last_exception}",
                            details={"endpoint": token_url, "error_type": type(last_exception).__name__},
                        )

        # If we get here, all retries exhausted
        if last_exception:
            raise self._map_httpx_error(last_exception, token_url)
        else:
            raise NetworkError(
                "Failed to refresh access token: unknown error",
                details={"endpoint": token_url},
            )

    def close(self) -> None:
        """Close HTTP client and clean up resources."""
        if self._http_client is not None:
            self._http_client.close()
            self._http_client = None

    def __enter__(self):
        """Context manager entry."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        self.close()

__init__

__init__(config_manager: Optional[ConfigurationManager] = None, retry_policy: Optional[RetryPolicy] = None, verify_ssl: bool = True) -> None

Initialize Keycloak client.

Parameters:

Name Type Description Default
config_manager Optional[ConfigurationManager]

Optional ConfigurationManager instance

None
retry_policy Optional[RetryPolicy]

Optional RetryPolicy (uses default from config if not provided)

None
verify_ssl bool

Whether to verify SSL certificates (default: True)

True
Source code in src/miura/auth/keycloak.py
def __init__(
    self,
    config_manager: Optional[ConfigurationManager] = None,
    retry_policy: Optional[RetryPolicy] = None,
    verify_ssl: bool = True,
) -> None:
    """
    Initialize Keycloak client.

    Args:
        config_manager: Optional ConfigurationManager instance
        retry_policy: Optional RetryPolicy (uses default from config if not provided)
        verify_ssl: Whether to verify SSL certificates (default: True)
    """
    self._config_manager = config_manager
    self._config: Optional[Dict[str, Any]] = None
    self._retry_policy = retry_policy
    self._verify_ssl = verify_ssl
    self._http_client: Optional["httpx.Client"] = None  # httpx.Client

_get_http_client

_get_http_client()

Get or create httpx client with connection pooling.

Source code in src/miura/auth/keycloak.py
def _get_http_client(self):
    """Get or create httpx client with connection pooling."""
    if self._http_client is None:
        import httpx

        # Get connection limits from config
        if self._config_manager:
            internal_config = self._config_manager.internal
            connection_limits = internal_config.http.default_connection_limits
        else:
            from miura.nexus.infrastructure.config import ConfigurationManager

            config_manager = ConfigurationManager(load_dotenv=False)
            internal_config = config_manager.internal
            connection_limits = internal_config.http.default_connection_limits

        # Create httpx client with connection pooling
        # Note: httpx automatically respects HTTP_PROXY, HTTPS_PROXY, NO_PROXY env vars
        # so we don't need to pass proxies explicitly
        self._http_client = httpx.Client(
            limits=httpx.Limits(
                max_connections=connection_limits.total,
                max_keepalive_connections=connection_limits.per_host,
            ),
            timeout=httpx.Timeout(
                connect=10.0,
                read=30.0,
                write=30.0,
                pool=30.0,
            ),
            headers={"User-Agent": _get_user_agent()},
            verify=self._verify_ssl,
        )
    return self._http_client

_get_retry_policy

_get_retry_policy() -> RetryPolicy

Get retry policy from config or use default.

Source code in src/miura/auth/keycloak.py
def _get_retry_policy(self) -> RetryPolicy:
    """Get retry policy from config or use default."""
    if self._retry_policy is not None:
        return self._retry_policy

    # Get from config manager if available
    if self._config_manager:
        # Check if there's a retry policy in config
        # For now, use default RetryPolicy
        return RetryPolicy()

    return RetryPolicy()

_get_config

_get_config() -> Dict[str, Any]

Resolve the effective Keycloak configuration from InternalConfig.

Returns:

Type Description
Dict[str, Any]

Dict[str, Any]: Mapping with keys base_url, realm, client_id, timeout_connect, timeout_read.

Raises:

Type Description
ValidationError

If configuration is invalid

Source code in src/miura/auth/keycloak.py
def _get_config(self) -> Dict[str, Any]:
    """
    Resolve the effective Keycloak configuration from InternalConfig.

    Returns:
        Dict[str, Any]: Mapping with keys base_url, realm, client_id, timeout_connect, timeout_read.

    Raises:
        ValidationError: If configuration is invalid
    """
    if self._config is not None:
        return self._config

    # Get config manager or create default
    if self._config_manager is None:
        from miura.nexus.infrastructure.config import ConfigurationManager

        self._config_manager = ConfigurationManager(load_dotenv=True)

    # Get Keycloak config from InternalConfig
    internal_config = self._config_manager.internal
    auth_config = internal_config.auth
    http_config = internal_config.http

    # Validate configuration
    if not auth_config.keycloak_base_url:
        raise ValidationError(
            "Keycloak base URL is not configured. Set MIURA_INTERNAL_KEYCLOAK_BASE_URL or use default.",
            details={"config_source": "InternalConfig.auth.keycloak_base_url"},
        )

    if not auth_config.keycloak_realm:
        raise ValidationError(
            "Keycloak realm is not configured. Set MIURA_INTERNAL_KEYCLOAK_REALM or use default.",
            details={"config_source": "InternalConfig.auth.keycloak_realm"},
        )

    if not auth_config.keycloak_client_id:
        raise ValidationError(
            "Keycloak client ID is not configured. Set MIURA_INTERNAL_KEYCLOAK_CLIENT_ID or use default.",
            details={"config_source": "InternalConfig.auth.keycloak_client_id"},
        )

    # Validate URL format
    base_url = auth_config.keycloak_base_url
    if not base_url.startswith(("http://", "https://")):
        raise ValidationError(
            f"Invalid Keycloak base URL format: {base_url}. Must start with http:// or https://",
            details={"base_url": base_url, "config_source": "InternalConfig.auth.keycloak_base_url"},
        )

    # Get timeout from shared Timeouts config
    timeouts = http_config.default_timeouts
    timeout_connect = timeouts.connect if timeouts else 10.0
    timeout_read = timeouts.read if timeouts else 30.0

    self._config = {
        "base_url": base_url,
        "realm": auth_config.keycloak_realm,
        "client_id": auth_config.keycloak_client_id,
        "timeout_connect": timeout_connect,
        "timeout_read": timeout_read,
    }
    return self._config

_map_httpx_error

_map_httpx_error(error: Exception, endpoint: str) -> Exception

Map httpx exceptions to shared exception hierarchy.

Parameters:

Name Type Description Default
error Exception

httpx exception

required
endpoint str

Endpoint URL for context

required

Returns:

Type Description
Exception

Appropriate NexusError subclass

Source code in src/miura/auth/keycloak.py
def _map_httpx_error(self, error: Exception, endpoint: str) -> Exception:
    """
    Map httpx exceptions to shared exception hierarchy.

    Args:
        error: httpx exception
        endpoint: Endpoint URL for context

    Returns:
        Appropriate NexusError subclass
    """
    import httpx

    if isinstance(error, httpx.TimeoutException):
        kc_config = self._get_config()
        timeout = kc_config.get("timeout_read", 30.0)
        return TimeoutError("refresh_access_token", timeout, details={"endpoint": endpoint})
    elif isinstance(error, httpx.ConnectError):
        return NetworkError(
            f"Failed to connect to Keycloak: {error}",
            details={"endpoint": endpoint, "error_type": type(error).__name__},
        )
    elif isinstance(error, httpx.HTTPStatusError):
        status_code = error.response.status_code if error.response else None

        # Try to extract Keycloak error details from response body
        keycloak_error = None
        keycloak_error_description = None
        if error.response:
            try:
                error_data = error.response.json()
                keycloak_error = error_data.get("error")
                keycloak_error_description = error_data.get("error_description")
            except (ValueError, KeyError, AttributeError):
                # Response is not JSON or doesn't have expected structure
                pass

        if status_code == 401:
            error_msg = "Authentication failed. Token may be expired or invalid."
            if keycloak_error_description:
                error_msg = f"Authentication failed: {keycloak_error_description}"
            return PermissionError(
                error_msg,
                details={
                    "endpoint": endpoint,
                    "status_code": status_code,
                    "keycloak_error": keycloak_error,
                    "keycloak_error_description": keycloak_error_description,
                },
            )
        elif status_code == 400:
            # 400 errors are typically client errors (invalid request, invalid token, etc.)
            # If we have error details, include them
            if keycloak_error and keycloak_error_description:
                error_msg = f"Keycloak error ({keycloak_error}): {keycloak_error_description}"
            elif keycloak_error:
                error_msg = f"Keycloak error: {keycloak_error}"
            else:
                error_msg = f"Bad request to Keycloak: {error}"
            return ApiKeyInvalidError(
                error_msg,
                details={
                    "endpoint": endpoint,
                    "status_code": status_code,
                    "keycloak_error": keycloak_error,
                    "keycloak_error_description": keycloak_error_description,
                },
            )
        elif status_code == 429:
            # Rate limit - retryable
            error_msg = "Rate limited by Keycloak"
            if keycloak_error_description:
                error_msg = f"Rate limited by Keycloak: {keycloak_error_description}"
            return NetworkError(
                error_msg,
                details={"endpoint": endpoint, "status_code": status_code},
            )
        elif status_code and status_code >= 500:
            # Backend/service error (not network) - use safe user-facing message
            from miura.sdk.exceptions import ServiceError

            reason = "The authentication service encountered an error. Please try again later."
            details_ = {"endpoint": endpoint, "status_code": status_code}
            if keycloak_error_description:
                details_["_raw_message"] = keycloak_error_description
            return ServiceError(reason, details_)
        else:
            error_msg = f"HTTP error from Keycloak: {error}"
            if keycloak_error_description:
                error_msg = f"Keycloak error: {keycloak_error_description}"
            return NetworkError(
                error_msg,
                details={
                    "endpoint": endpoint,
                    "status_code": status_code,
                    "keycloak_error": keycloak_error,
                    "keycloak_error_description": keycloak_error_description,
                },
            )
    elif isinstance(error, httpx.RequestError):
        return NetworkError(
            f"Request error: {error}",
            details={"endpoint": endpoint, "error_type": type(error).__name__},
        )
    else:
        return NetworkError(
            f"Unexpected error: {error}",
            details={"endpoint": endpoint, "error_type": type(error).__name__},
        )

refresh_access_token

refresh_access_token(refresh_token: str) -> str

Use refresh token to get a fresh access token from Keycloak.

Implements retry logic using shared RetryPolicy.

Parameters:

Name Type Description Default
refresh_token str

The refresh token to use

required

Returns:

Name Type Description
str str

Fresh access token

Raises:

Type Description
PermissionError

If authentication fails (401)

NetworkError

If network request fails

TimeoutError

If request times out

ApiKeyInvalidError

If token is invalid

Source code in src/miura/auth/keycloak.py
def refresh_access_token(self, refresh_token: str) -> str:
    """
    Use refresh token to get a fresh access token from Keycloak.

    Implements retry logic using shared RetryPolicy.

    Args:
        refresh_token: The refresh token to use

    Returns:
        str: Fresh access token

    Raises:
        PermissionError: If authentication fails (401)
        NetworkError: If network request fails
        TimeoutError: If request times out
        ApiKeyInvalidError: If token is invalid
    """
    import httpx

    # Get configuration
    kc_config = self._get_config()
    kc_base_url = kc_config["base_url"]
    kc_realm = kc_config["realm"]
    kc_client_id = kc_config["client_id"]
    timeout_connect = kc_config["timeout_connect"]
    timeout_read = kc_config["timeout_read"]

    token_url = f"{kc_base_url}/realms/{kc_realm}/protocol/openid-connect/token"

    # Prepare the request
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    data = {
        "grant_type": "refresh_token",
        "client_id": kc_client_id,
        "refresh_token": refresh_token,
    }

    logger.debug(f"Refreshing access token using Keycloak endpoint: {token_url}")

    # Get retry policy
    retry_policy = self._get_retry_policy()

    # Get HTTP client
    http_client = self._get_http_client()

    # Retry loop using shared RetryPolicy
    last_exception: Optional[Exception] = None
    for attempt in range(retry_policy.max_retries + 1):
        try:
            # Make the request with timeout
            # httpx.Timeout requires either a default or all four parameters (connect, read, write, pool)
            # Use a default timeout value (total timeout) which applies to all operations
            total_timeout = timeout_connect + timeout_read
            timeout = httpx.Timeout(total_timeout)
            response = http_client.post(
                token_url,
                headers=headers,
                data=data,
                timeout=timeout,
            )
            response.raise_for_status()

            # Parse the response
            token_data = response.json()
            access_token = token_data.get("access_token")

            if not access_token:
                raise ApiKeyInvalidError(
                    "No access token received from Keycloak",
                    details={"endpoint": token_url},
                )

            logger.debug("Successfully refreshed access token")
            return access_token

        except httpx.HTTPStatusError as e:
            # Check if it's a 401 (auth error - don't retry)
            if e.response and e.response.status_code == 401:
                raise self._map_httpx_error(e, token_url)
            # Check if it's a 400 with invalid_token (token validation error - don't retry)
            if e.response and e.response.status_code == 400:
                try:
                    error_data = e.response.json()
                    error_code = error_data.get("error", "")
                    error_description = error_data.get("error_description", "")

                    # Keycloak returns "invalid_token" for invalid refresh tokens
                    if error_code == "invalid_token":
                        default_msg = "The refresh token is expired, revoked, or malformed."
                        error_msg = f"Invalid refresh token. {error_description or default_msg}"
                        raise ApiKeyInvalidError(
                            error_msg,
                            details={
                                "endpoint": token_url,
                                "status_code": 400,
                                "keycloak_error": error_code,
                                "keycloak_error_description": error_description,
                            },
                        )
                except (ValueError, KeyError, AttributeError):
                    # If we can't parse the response, fall through to generic handling
                    pass
            # Other HTTP errors might be retryable
            last_exception = e
        except (httpx.TimeoutException, httpx.ConnectError, httpx.RequestError) as e:
            # Network/timeout errors - retryable
            last_exception = e
        except ApiKeyInvalidError:
            # Don't retry invalid token errors - re-raise immediately
            raise
        except (KeyError, ValueError) as e:
            # Invalid response - don't retry
            logger.error(f"Invalid response from Keycloak: {e}")
            raise ApiKeyInvalidError(
                f"Invalid response from Keycloak: {e}",
                details={"endpoint": token_url},
            )
        except Exception as e:
            # Unexpected errors - check if retryable
            last_exception = e

        # Check if we should retry
        # Map httpx exceptions to NetworkError/TimeoutError for retry check
        retry_exception = last_exception
        if isinstance(last_exception, (httpx.TimeoutException, httpx.ConnectError, httpx.RequestError)):
            # These are network errors - check retry using a NetworkError
            from miura.sdk.exceptions import NetworkError, TimeoutError

            if isinstance(last_exception, httpx.TimeoutException):
                retry_exception = TimeoutError("refresh_access_token", 30.0, details={})
            else:
                retry_exception = NetworkError(str(last_exception), details={})

        if last_exception and retry_policy.should_retry(retry_exception, attempt):
            # Extract Retry-After header if present
            retry_after = None
            if isinstance(last_exception, httpx.HTTPStatusError) and last_exception.response:
                retry_after_header = last_exception.response.headers.get("Retry-After")
                if retry_after_header:
                    try:
                        retry_after = float(retry_after_header)
                    except (ValueError, TypeError):
                        pass

            # Calculate backoff using shared RetryPolicy
            backoff = retry_policy.calculate_backoff(attempt, retry_after)

            # Wait before retry
            time.sleep(backoff)
        else:
            # Don't retry - map error and raise
            if last_exception:
                if isinstance(last_exception, httpx.HTTPStatusError):
                    raise self._map_httpx_error(last_exception, token_url)
                elif isinstance(last_exception, (httpx.TimeoutException, httpx.ConnectError, httpx.RequestError)):
                    raise self._map_httpx_error(last_exception, token_url)
                else:
                    raise NetworkError(
                        f"Failed to refresh access token: {last_exception}",
                        details={"endpoint": token_url, "error_type": type(last_exception).__name__},
                    )

    # If we get here, all retries exhausted
    if last_exception:
        raise self._map_httpx_error(last_exception, token_url)
    else:
        raise NetworkError(
            "Failed to refresh access token: unknown error",
            details={"endpoint": token_url},
        )

close

close() -> None

Close HTTP client and clean up resources.

Source code in src/miura/auth/keycloak.py
def close(self) -> None:
    """Close HTTP client and clean up resources."""
    if self._http_client is not None:
        self._http_client.close()
        self._http_client = None

__enter__

__enter__()

Context manager entry.

Source code in src/miura/auth/keycloak.py
def __enter__(self):
    """Context manager entry."""
    return self

__exit__

__exit__(exc_type, exc_val, exc_tb)

Context manager exit.

Source code in src/miura/auth/keycloak.py
def __exit__(self, exc_type, exc_val, exc_tb):
    """Context manager exit."""
    self.close()

TokenManager

Manages authentication tokens in memory.

Source code in src/miura/auth/token.py
class TokenManager:
    """Manages authentication tokens in memory."""

    def __init__(self) -> None:
        self._refresh_token: Optional[str] = None
        self._access_token: Optional[str] = None

    @property
    def refresh_token(self) -> Optional[str]:
        """Get the current refresh token."""
        return self._refresh_token

    @property
    def access_token(self) -> Optional[str]:
        """Get the current access token."""
        return self._access_token

    def set_access_token(self, token: Optional[str]) -> None:
        """Update the cached access token."""
        self._access_token = token

    def invalidate_access_token(self, reason: str) -> None:
        """Clear the cached access token with an optional log."""
        if self._access_token is not None:
            logger.debug(reason)
        self.set_access_token(None)

    def set_refresh_token(
        self, token: Optional[str], *, reason: Optional[str] = None, force_invalidate: bool = False
    ) -> Optional[str]:
        """
        Update the cached refresh token and, when appropriate, invalidate the access token cache.

        Args:
            token: New refresh token value (or None to clear).
            reason: Optional message to log when invalidating the access token.
            force_invalidate: When True, clears cached access token even if the refresh token is unchanged.
        """
        if not force_invalidate and token == self._refresh_token:
            return self._refresh_token

        if reason:
            self.invalidate_access_token(reason)
        else:
            self.invalidate_access_token("Refresh token updated, clearing cached access token")

        self._refresh_token = token
        return self._refresh_token

    def clear_all(self, reason: str = "Clearing cached tokens from memory") -> None:
        """Clear all cached tokens."""
        self.set_refresh_token(None, reason=reason, force_invalidate=True)

    def has_tokens(self) -> bool:
        """Check if any tokens are currently cached."""
        return self._refresh_token is not None or self._access_token is not None

refresh_token property

refresh_token: Optional[str]

Get the current refresh token.

access_token property

access_token: Optional[str]

Get the current access token.

set_access_token

set_access_token(token: Optional[str]) -> None

Update the cached access token.

Source code in src/miura/auth/token.py
def set_access_token(self, token: Optional[str]) -> None:
    """Update the cached access token."""
    self._access_token = token

invalidate_access_token

invalidate_access_token(reason: str) -> None

Clear the cached access token with an optional log.

Source code in src/miura/auth/token.py
def invalidate_access_token(self, reason: str) -> None:
    """Clear the cached access token with an optional log."""
    if self._access_token is not None:
        logger.debug(reason)
    self.set_access_token(None)

set_refresh_token

set_refresh_token(token: Optional[str], *, reason: Optional[str] = None, force_invalidate: bool = False) -> Optional[str]

Update the cached refresh token and, when appropriate, invalidate the access token cache.

Parameters:

Name Type Description Default
token Optional[str]

New refresh token value (or None to clear).

required
reason Optional[str]

Optional message to log when invalidating the access token.

None
force_invalidate bool

When True, clears cached access token even if the refresh token is unchanged.

False
Source code in src/miura/auth/token.py
def set_refresh_token(
    self, token: Optional[str], *, reason: Optional[str] = None, force_invalidate: bool = False
) -> Optional[str]:
    """
    Update the cached refresh token and, when appropriate, invalidate the access token cache.

    Args:
        token: New refresh token value (or None to clear).
        reason: Optional message to log when invalidating the access token.
        force_invalidate: When True, clears cached access token even if the refresh token is unchanged.
    """
    if not force_invalidate and token == self._refresh_token:
        return self._refresh_token

    if reason:
        self.invalidate_access_token(reason)
    else:
        self.invalidate_access_token("Refresh token updated, clearing cached access token")

    self._refresh_token = token
    return self._refresh_token

clear_all

clear_all(reason: str = 'Clearing cached tokens from memory') -> None

Clear all cached tokens.

Source code in src/miura/auth/token.py
def clear_all(self, reason: str = "Clearing cached tokens from memory") -> None:
    """Clear all cached tokens."""
    self.set_refresh_token(None, reason=reason, force_invalidate=True)

has_tokens

has_tokens() -> bool

Check if any tokens are currently cached.

Source code in src/miura/auth/token.py
def has_tokens(self) -> bool:
    """Check if any tokens are currently cached."""
    return self._refresh_token is not None or self._access_token is not None

Result

Bases: BaseModel

Result schema for command output.

Source code in src/miura/auth/models.py
class Result(BaseModel):
    """Result schema for command output."""

    status: Literal["success", "error"] = Field(description="Operation status")
    code: Optional[str] = Field(default=None, description="Error code if error")
    message: str = Field(description="Human-readable message")
    details: Dict[str, Any] = Field(default_factory=dict, description="Additional details")

TokenDetails

Bases: BaseModel

Token details schema for JSON output.

Source code in src/miura/auth/models.py
class TokenDetails(BaseModel):
    """Token details schema for JSON output."""

    issuer: Optional[str] = Field(default=None, description="Token issuer (from JWT)")
    subject: Optional[str] = Field(default=None, description="Token subject (user ID)")
    audience: Optional[str] = Field(default=None, description="Token audience")
    scopes: List[str] = Field(default_factory=list, description="Token scopes")
    issued_at: Optional[float] = Field(default=None, description="Token issued timestamp")
    expires_at: Optional[float] = Field(default=None, description="Token expiration timestamp")
    expires_in: Optional[int] = Field(default=None, description="Seconds until expiration")
    storage: Dict[str, Any] = Field(
        default_factory=dict, description="Storage backend information"
    )  # {"backend": "file|keyring|none", "path": str|None}
    user_info: Dict[str, Any] = Field(
        default_factory=dict, description="User information from token"
    )  # {"name": str, "email": str, "username": str}

auth

auth(ctx, verbose, no_input, storage, json, no_color)

Manage authentication api keys.

Source code in src/miura/auth/cli.py
@click.group()
@click.option(
    "-v",
    "--verbose",
    is_flag=True,
    help="Enable verbose logging (show debug messages)",
)
@click.option(
    "--no-input",
    is_flag=True,
    help="Non-interactive mode (no prompts, for CI/CD)",
)
@click.option(
    "-s",
    "--storage",
    type=click.Choice(["file", "keyring"], case_sensitive=False),
    default="file",
    help="Storage backend for tokens: file (default) or keyring",
)
@click.option(
    "--json",
    is_flag=True,
    help="Output in JSON format (for scripting)",
)
@click.option(
    "--no-color",
    is_flag=True,
    help="Disable colored output",
)
@click.pass_context
def auth(ctx, verbose, no_input, storage, json, no_color):
    """Manage authentication api keys."""
    from miura.logging import configure_logging
    from miura.logging.config import LoggingConfig

    # Store flags in context for commands to use
    ctx.ensure_object(dict)
    ctx.obj["verbose"] = verbose
    ctx.obj["no_input"] = no_input or (os.environ.get("MIURA_AUTH_NO_INPUT", "").lower() in ("1", "true", "yes"))
    ctx.obj["storage"] = storage or os.environ.get("MIURA_AUTH_STORAGE", "file")
    ctx.obj["json"] = json
    ctx.obj["no_color"] = no_color or (os.environ.get("NO_COLOR", "").lower() in ("1", "true", "yes"))

    # Configure logging using LoggingConfig (shared with main API)
    logging_config = LoggingConfig.from_env()
    if verbose:
        # Create new config with verbose enabled
        logging_config = LoggingConfig(
            verbose=True,
            level=logging_config.level,
            format=logging_config.format,
            timestamp=logging_config.timestamp,
            timestamp_format=logging_config.timestamp_format,
            request_id=logging_config.request_id,
            logger_name=logging_config.logger_name,
        )
    configure_logging(logging_config)

get_exit_code

get_exit_code(error: NexusError) -> int

Map NexusError codes to deterministic exit codes.

Exit codes: - 0: Success - 1: Generic error - 2: Usage/non-interactive violation - 3: Auth invalid/expired - 4: Network/timeout - 5: Keyring/backend error - 6: Config invalid

Parameters:

Name Type Description Default
error NexusError

NexusError instance

required

Returns:

Type Description
int

Exit code integer

Source code in src/miura/auth/exceptions.py
def get_exit_code(error: NexusError) -> int:
    """
    Map NexusError codes to deterministic exit codes.

    Exit codes:
    - 0: Success
    - 1: Generic error
    - 2: Usage/non-interactive violation
    - 3: Auth invalid/expired
    - 4: Network/timeout
    - 5: Keyring/backend error
    - 6: Config invalid

    Args:
        error: NexusError instance

    Returns:
        Exit code integer
    """
    code_map = {
        ErrorCode.TOKEN_EXPIRED: 3,
        ErrorCode.TOKEN_INVALID: 3,
        ErrorCode.AUTH_ERROR: 3,
        ErrorCode.PERMISSION_DENIED: 3,
        ErrorCode.NETWORK_ERROR: 4,
        ErrorCode.TIMEOUT: 4,
        ErrorCode.KEYRING_ERROR: 5,
        ErrorCode.CONFIG_ERROR: 6,
        ErrorCode.VALIDATION_ERROR: 6,
    }
    return code_map.get(error.code, 1)

get_auth_api_key

get_auth_api_key(storage_backend: Optional[str] = None) -> str

Get an access token for API authentication.

SSL verification for Keycloak can be controlled via MIURA_KEYCLOAK_VERIFY_SSL environment variable. Defaults to True if not set.

Source code in src/miura/auth/handler.py
def get_auth_api_key(storage_backend: Optional[str] = None) -> str:
    """Get an access token for API authentication.

    SSL verification for Keycloak can be controlled via MIURA_KEYCLOAK_VERIFY_SSL environment variable.
    Defaults to True if not set.
    """
    # Read Keycloak SSL verification from environment variable
    verify_ssl = True
    verify_ssl_env = os.environ.get("MIURA_KEYCLOAK_VERIFY_SSL", "").lower()
    if verify_ssl_env in ("false", "0", "no", "off"):
        verify_ssl = False

    handler = AuthHandler(storage_backend=storage_backend, verify_ssl=verify_ssl)
    # Pass storage backend type to sync method
    from .storage import KeyringStorageBackend, FileStorageBackend, NoneStorageBackend

    storage_type = None
    if isinstance(handler.storage_backend, KeyringStorageBackend):
        storage_type = "keyring"
    elif isinstance(handler.storage_backend, FileStorageBackend):
        storage_type = "file"
    elif isinstance(handler.storage_backend, NoneStorageBackend):
        storage_type = "none"
    handler._sync_refresh_token_from_sources(storage_backend_type=storage_type)
    return handler.get_auth_api_key()

get_nexus_refresh_token

get_nexus_refresh_token(storage_backend: Optional[str] = None) -> Optional[str]

Resolve the refresh/offline JWT used for Nexus API auth (env, then miura-auth storage).

Does not call Keycloak; same source order as :meth:AuthHandler.get_auth_api_key uses before refreshing access tokens. Intended for callers that must forward this credential (e.g. user-delegated DPD recipe execution).

Source code in src/miura/auth/handler.py
def get_nexus_refresh_token(storage_backend: Optional[str] = None) -> Optional[str]:
    """Resolve the refresh/offline JWT used for Nexus API auth (env, then miura-auth storage).

    Does not call Keycloak; same source order as :meth:`AuthHandler.get_auth_api_key` uses
    before refreshing access tokens. Intended for callers that must forward this credential
    (e.g. user-delegated DPD recipe execution).
    """
    verify_ssl = True
    verify_ssl_env = os.environ.get("MIURA_KEYCLOAK_VERIFY_SSL", "").lower()
    if verify_ssl_env in ("false", "0", "no", "off"):
        verify_ssl = False

    handler = AuthHandler(storage_backend=storage_backend, verify_ssl=verify_ssl)
    from .storage import KeyringStorageBackend, FileStorageBackend, NoneStorageBackend

    storage_type = None
    if isinstance(handler.storage_backend, KeyringStorageBackend):
        storage_type = "keyring"
    elif isinstance(handler.storage_backend, FileStorageBackend):
        storage_type = "file"
    elif isinstance(handler.storage_backend, NoneStorageBackend):
        storage_type = "none"
    return handler._sync_refresh_token_from_sources(storage_backend_type=storage_type)

set_auth_api_key

set_auth_api_key(api_key: str, storage_backend: Optional[str] = None) -> None

Store a refresh token in storage backend.

Source code in src/miura/auth/handler.py
def set_auth_api_key(api_key: str, storage_backend: Optional[str] = None) -> None:
    """Store a refresh token in storage backend."""
    # verify_ssl=None means AuthHandler will read from MIURA_KEYCLOAK_VERIFY_SSL env var
    handler = AuthHandler(storage_backend=storage_backend, verify_ssl=None)
    return handler.set_auth_api_key(api_key)

clear_api_keys

clear_api_keys(storage_backend: Optional[str] = None) -> None

Clear all stored tokens from memory and storage backend.

Source code in src/miura/auth/handler.py
def clear_api_keys(storage_backend: Optional[str] = None) -> None:
    """Clear all stored tokens from memory and storage backend."""
    # verify_ssl=None means AuthHandler will read from MIURA_KEYCLOAK_VERIFY_SSL env var
    handler = AuthHandler(storage_backend=storage_backend, verify_ssl=None)
    return handler.clear_api_keys()

get_api_key_info

get_api_key_info(storage_backend: Optional[str] = None) -> Dict[str, Any]

Get information about the current refresh token and access token.

Source code in src/miura/auth/handler.py
def get_api_key_info(storage_backend: Optional[str] = None) -> Dict[str, Any]:
    """Get information about the current refresh token and access token."""
    # verify_ssl=None means AuthHandler will read from MIURA_KEYCLOAK_VERIFY_SSL env var
    handler = AuthHandler(storage_backend=storage_backend, verify_ssl=None)
    return handler.get_api_key_info()

get_api_key_details

get_api_key_details(storage_backend: Optional[str] = None) -> Dict[str, Any]

Get detailed information about the current refresh token including JWT payload.

Source code in src/miura/auth/handler.py
def get_api_key_details(storage_backend: Optional[str] = None) -> Dict[str, Any]:
    """Get detailed information about the current refresh token including JWT payload."""
    # verify_ssl=None means AuthHandler will read from MIURA_KEYCLOAK_VERIFY_SSL env var
    handler = AuthHandler(storage_backend=storage_backend, verify_ssl=None)
    return handler.get_api_key_details()

redact_token

redact_token(token: Optional[str]) -> str

Redact a token for safe display in logs and output.

Parameters:

Name Type Description Default
token Optional[str]

Token string to redact

required

Returns:

Type Description
str

Redacted token string (first 4, last 4 chars) or placeholder

Source code in src/miura/auth/utils.py
def redact_token(token: Optional[str]) -> str:
    """
    Redact a token for safe display in logs and output.

    Args:
        token: Token string to redact

    Returns:
        Redacted token string (first 4, last 4 chars) or placeholder
    """
    if not token:
        return "[NOT SET]"
    if len(token) <= 8:
        return "[REDACTED]"
    return f"{token[:4]}...{token[-4:]}"

set_api_key

set_api_key(api_key: str, storage: Literal['file', 'keyring', 'none'] = 'file') -> Result

Set API key programmatically.

Parameters:

Name Type Description Default
api_key str

API key to store

required
storage Literal['file', 'keyring', 'none']

Storage backend to use

'file'

Returns:

Type Description
Result

Result with status and message

Raises:

Type Description
ApiKeyInvalidError

If token format is invalid

KeyringError

If storage fails

Source code in src/miura/auth/core.py
def set_api_key(
    api_key: str,
    storage: Literal["file", "keyring", "none"] = "file",
) -> Result:
    """
    Set API key programmatically.

    Args:
        api_key: API key to store
        storage: Storage backend to use

    Returns:
        Result with status and message

    Raises:
        ApiKeyInvalidError: If token format is invalid
        KeyringError: If storage fails
    """
    # verify_ssl=None means AuthHandler will read from MIURA_KEYCLOAK_VERIFY_SSL env var
    handler = AuthHandler(storage_backend=storage, verify_ssl=None)
    try:
        handler.set_auth_api_key(api_key)
        return Result(status="success", message="API key stored successfully")
    except (ApiKeyInvalidError, KeyringError, NexusError) as e:
        return Result(
            status="error",
            code=getattr(e, "code", "unknown"),
            message=str(e),
            details=getattr(e, "details", {}),
        )

get_token_details

get_token_details(storage: Literal['file', 'keyring', 'none'] = 'file') -> TokenDetails

Get token details programmatically.

Parameters:

Name Type Description Default
storage Literal['file', 'keyring', 'none']

Storage backend to use

'file'

Returns:

Type Description
TokenDetails

TokenDetails with all token information

Raises:

Type Description
ApiKeyNotFoundError

If no token is found

ApiKeyInvalidError

If token is invalid

Source code in src/miura/auth/core.py
def get_token_details(
    storage: Literal["file", "keyring", "none"] = "file",
) -> TokenDetails:
    """
    Get token details programmatically.

    Args:
        storage: Storage backend to use

    Returns:
        TokenDetails with all token information

    Raises:
        ApiKeyNotFoundError: If no token is found
        ApiKeyInvalidError: If token is invalid
    """
    import time as time_module

    # verify_ssl=None means AuthHandler will read from MIURA_KEYCLOAK_VERIFY_SSL env var
    handler = AuthHandler(storage_backend=storage, verify_ssl=None)
    token_details = handler.get_api_key_details()

    jwt_payload = token_details.get("jwt_payload", {})
    expires_at = jwt_payload.get("exp")
    expires_in = None
    if expires_at:
        expires_in = int(expires_at - time_module.time())

    return TokenDetails(
        issuer=jwt_payload.get("iss"),
        subject=jwt_payload.get("sub"),
        audience=jwt_payload.get("aud"),
        scopes=jwt_payload.get("scope", "").split() if jwt_payload.get("scope") else [],
        issued_at=jwt_payload.get("iat"),
        expires_at=expires_at,
        expires_in=expires_in if expires_in and expires_in > 0 else None,
        storage={
            "backend": storage,
            "path": None,  # Could add path for file backend
        },
        user_info={
            "name": token_details.get("name"),
            "email": token_details.get("email"),
            "username": token_details.get("preferred_username"),
        },
    )

delete_api_key

delete_api_key(storage: Literal['file', 'keyring', 'none'] = 'file') -> Result

Delete API key programmatically.

Parameters:

Name Type Description Default
storage Literal['file', 'keyring', 'none']

Storage backend to use

'file'

Returns:

Type Description
Result

Result with status and message

Source code in src/miura/auth/core.py
def delete_api_key(
    storage: Literal["file", "keyring", "none"] = "file",
) -> Result:
    """
    Delete API key programmatically.

    Args:
        storage: Storage backend to use

    Returns:
        Result with status and message
    """
    # verify_ssl=None means AuthHandler will read from MIURA_KEYCLOAK_VERIFY_SSL env var
    handler = AuthHandler(storage_backend=storage, verify_ssl=None)
    try:
        handler.clear_api_keys()
        handler._clear_keyring_cache()
        return Result(status="success", message="API key deleted successfully")
    except (KeyringError, NexusError) as e:
        return Result(
            status="error",
            code=getattr(e, "code", "unknown"),
            message=str(e),
            details=getattr(e, "details", {}),
        )

test_auth

test_auth(ping_api: bool = False, storage: Literal['file', 'keyring', 'none'] = 'file') -> Result

Test authentication programmatically.

Parameters:

Name Type Description Default
ping_api bool

If True, test API connectivity in addition to auth

False
storage Literal['file', 'keyring', 'none']

Storage backend to use

'file'

Returns:

Type Description
Result

Result with status and message

Raises:

Type Description
ApiKeyNotFoundError

If no token is found

ApiKeyInvalidError

If token is invalid

NetworkError

If API connectivity test fails

Source code in src/miura/auth/core.py
def test_auth(
    ping_api: bool = False,
    storage: Literal["file", "keyring", "none"] = "file",
) -> Result:
    """
    Test authentication programmatically.

    Args:
        ping_api: If True, test API connectivity in addition to auth
        storage: Storage backend to use

    Returns:
        Result with status and message

    Raises:
        ApiKeyNotFoundError: If no token is found
        ApiKeyInvalidError: If token is invalid
        NetworkError: If API connectivity test fails
    """
    # verify_ssl=None means AuthHandler will read from MIURA_KEYCLOAK_VERIFY_SSL env var
    handler = AuthHandler(storage_backend=storage, verify_ssl=None)
    try:
        access_token = handler.get_auth_api_key()

        if ping_api:
            # Make actual API call to test connectivity
            from miura.nexus.infrastructure.api.api_endpoints import APIEndpoints, get_endpoint
            from miura.nexus.infrastructure.config import ConfigurationManager
            from miura.nexus.infrastructure.http.http_client import HTTPClient

            config_manager = ConfigurationManager(load_dotenv=True)
            internal_config = config_manager.internal
            base_url = internal_config.http.base_url
            test_url = get_endpoint(base_url, APIEndpoints.PROJECT_LIST)

            # Create HTTP client for API test
            def get_token():
                return access_token

            http_client = HTTPClient(
                get_auth_api_key=get_token,
                base_url=base_url,
                verify_ssl=True,
                config_manager=config_manager,
            )

            # Make a simple API call
            import asyncio

            async def test_api():
                await http_client.ensure_session()
                try:
                    await http_client.get(test_url)
                    await http_client.close()
                    return True
                except Exception:
                    await http_client.close()
                    raise

            try:
                asyncio.run(test_api())
            except Exception as api_error:
                if isinstance(api_error, (NetworkError, PermissionError)):
                    raise
                raise NetworkError(
                    f"API connectivity test failed: {api_error}",
                    details={"endpoint": test_url},
                )

        return Result(status="success", message="Authentication successful")
    except (ApiKeyNotFoundError, ApiKeyInvalidError, KeyringError, NexusError) as e:
        return Result(
            status="error",
            code=getattr(e, "code", "unknown"),
            message=str(e),
            details=getattr(e, "details", {}),
        )

get_user_info

get_user_info(storage: Literal['file', 'keyring', 'none'] = 'file') -> dict

Get current user identity programmatically.

Parameters:

Name Type Description Default
storage Literal['file', 'keyring', 'none']

Storage backend to use

'file'

Returns:

Type Description
dict

Dictionary with user information (name, email, username)

Raises:

Type Description
ApiKeyNotFoundError

If no token is found

ApiKeyInvalidError

If token is invalid

Source code in src/miura/auth/core.py
def get_user_info(
    storage: Literal["file", "keyring", "none"] = "file",
) -> dict:
    """
    Get current user identity programmatically.

    Args:
        storage: Storage backend to use

    Returns:
        Dictionary with user information (name, email, username)

    Raises:
        ApiKeyNotFoundError: If no token is found
        ApiKeyInvalidError: If token is invalid
    """
    # verify_ssl=None means AuthHandler will read from MIURA_KEYCLOAK_VERIFY_SSL env var
    handler = AuthHandler(storage_backend=storage, verify_ssl=None)
    token_details = handler.get_api_key_details()

    return {
        "name": token_details.get("name"),
        "email": token_details.get("email"),
        "username": token_details.get("preferred_username"),
    }

__getattr__

__getattr__(name: str)

Lazy import KeyringManager when accessed.

Source code in src/miura/auth/__init__.py
def __getattr__(name: str):
    """Lazy import KeyringManager when accessed."""
    if name == "KeyringManager":
        from .keyring import KeyringManager

        return KeyringManager
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")