Coverage for mcpgateway/utils/services_auth.py: 100%

1# -*- coding: utf-8 -*-

2"""mcpgateway.utils.services_auth - Authentication utilities for MCP Gateway

3

4 Copyright 2025

5 SPDX-License-Identifier: Apache-2.0

6 Authors: Mihai Criveti

7

8"""

9

10# Standard

11 import base64

12 import hashlib

13 import json

14 import os

15

16# Third-Party

17 from cryptography.hazmat.primitives.ciphers.aead import AESGCM

18

19# First-Party

20 from mcpgateway.config import settings

21

22

23 def get_key() -> bytes:

24 """

25 Generate a 32-byte AES encryption key derived from a passphrase.

26

27 Returns:

28 bytes: A 32-byte encryption key.

29

30 Raises:

31 ValueError: If the passphrase is not set or empty.

32 """

33 passphrase = settings.auth_encryption_secret

34 if not passphrase:

35 raise ValueError("AUTH_ENCRPYPTION_SECRET not set in environment.")

36 return hashlib.sha256(passphrase.encode()).digest() # 32-byte key

37

38

39 def encode_auth(auth_value: dict) -> str:

40 """

41 Encrypt and encode an authentication dictionary into a compact base64-url string.

42

43 Args:

44 auth_value (dict): The authentication dictionary to encrypt and encode.

45

46 Returns:

47 str: A base64-url-safe encrypted string representing the dictionary, or None if input is None.

48 """

49 if not auth_value:

50 return None

51 plaintext = json.dumps(auth_value)

52 key = get_key()

53 aesgcm = AESGCM(key)

54 nonce = os.urandom(12)

55 ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None)

56 combined = nonce + ciphertext

57 encoded = base64.urlsafe_b64encode(combined).rstrip(b"=")

58 return encoded.decode()

59

60

61 def decode_auth(encoded_value: str) -> dict:

62 """

63 Decode and decrypt a base64-url-safe encrypted string back into the authentication dictionary.

64

65 Args:

66 encoded_value (str): The encrypted base64-url string to decode and decrypt.

67

68 Returns:

69 dict: The decrypted authentication dictionary, or empty dict if input is None.

70 """

71 if not encoded_value:

72 return {}

73 key = get_key()

74 aesgcm = AESGCM(key)

75 # Fix base64 padding

76 padded = encoded_value + "=" * (-len(encoded_value) % 4)

77 combined = base64.urlsafe_b64decode(padded)

78 nonce = combined[:12]

79 ciphertext = combined[12:]

80 plaintext = aesgcm.decrypt(nonce, ciphertext, None)

81 return json.loads(plaintext.decode())