DefectDojo/django-DefectDojo

jwt.exceptions.InvalidKeyError: Could not parse the provided public key.

Closed this issue · 3 comments

Hello DefectDojo team,

This is my first time reaching out to the support team. I wanted to start by mentioning that I'm not entirely sure if what I'm experiencing is an actual bug or if it might be due to a misconfiguration on my part.

Be informative

I'm trying to integrate DefectDojo's authentication with Keycloak following the official documentation at https://defectdojo.github.io/django-DefectDojo/integrations/social-authentication/#keycloak.

Bug description

When testing the integration, I'm receiving the following error in the browser:

Well...
...this was unexpected.
500 Internal Server Error

I have defined the environment variable DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY using the step 6 of the documentation, but Keycloak don't add the header and footer.

DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs0YjRFE5Rr31dKuMjwu7BSrk2o2QN8Yp9xLH9QHT23HyymwN5fcmTkSvCpcrJu3/GuBqP7LOcacX2eeBnA2fUmzR7lPOLJ1eGgnXntju00K2VXHeyC0K5DZistMvGvHJyJQpnXRxutbsOeW8tpKc0GPUF8xvucJevlU5YwNnEEn1+8SRgqA553GUaiX397QXWUyVOlK/rQB/UmoyQyMJZJJyELOO/Tzj1vRXBgfADD9ZWPhXHtUaO+JsjYn0xV4qZTh9Ax1KY9bdC8hRRs0dFziTRgBOovH555Yue1q4q63XqvI+DBizG/TmMsU6pegs74JP74u5KB6/WMYChxZMgwIDAQAB

Analysing the logs and reviewing the expected public key format in the cryptography documentation (https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file), I've adjusted my Terraform configuration (redacted to contain a minimum of information):

terraform {
  required_providers {
    keycloak = {
      source  = "mrparkers/keycloak"
      version = "~> 4.4"
    }
  }
}

locals {
  raw_lines = trimspace(data.keycloak_realm_keys.realm_keys.keys[0].public_key)
  formatted_lines = regexall(".{1,64}", local.raw_lines)
  formatted_public_key = trimspace(join("\n", concat(
    ["-----BEGIN PUBLIC KEY-----"],
    local.formatted_lines,
    ["-----END PUBLIC KEY-----"]
  )))
}

data "keycloak_realm_keys" "realm_keys" {
  realm_id   = data.keycloak_realm.realm.id
  algorithms = ["RS256"]
  status     = ["ACTIVE"]
}

resource "kubectl_manifest" "defectdojo_configmap" {
  yaml_body = <<YAML
apiVersion: v1
kind: ConfigMap
metadata:
  name: defectdojo-configmap
  namespace: defectdojo
data:
  ...
  DD_SESSION_COOKIE_SECURE: "true"
  DD_CSRF_COOKIE_SECURE: "true"
  DD_SECURE_SSL_REDIRECT: "true"
  DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_ENABLED: "true"
  DD_SOCIAL_AUTH_KEYCLOAK_KEY: "${keycloak_openid_client.defectdojo_client.client_id}"
  DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY: |-
    ${replace(local.formatted_public_key, "\n", "\n    ")}
  ...
YAML
}

The result of this configuration is

defectdojo-django-6b9547b696-tx4mk:/app$ printenv
...
DD_UWSGI_HOST=localhost
DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs0YjRFE5Rr31dKuMjwu7
BSrk2o2QN8Yp9xLH9QHT23HyymwN5fcmTkSvCpcrJu3/GuBqP7LOcacX2eeBnA2f
UmzR7lPOLJ1eGgnXntju00K2VXHeyC0K5DZistMvGvHJyJQpnXRxutbsOeW8tpKc
0GPUF8xvucJevlU5YwNnEEn1+8SRgqA553GUaiX397QXWUyVOlK/rQB/UmoyQyMJ
ZJJyELOO/Tzj1vRXBgfADD9ZWPhXHtUaO+JsjYn0xV4qZTh9Ax1KY9bdC8hRRs0d
FziTRgBOovH555Yue1q4q63XqvI+DBizG/TmMsU6pegs74JP74u5KB6/WMYChxZM
gwIDAQAB
-----END PUBLIC KEY-----
DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_ENABLED=true
...

But the issue persists. Trying to figure out if the public key is valid, I saved the environment variable to a file and execute the existent openssl command in the pod:

defectdojo-django-6b9547b696-tx4mk:/app$ echo -e "$DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY" > /tmp/pub_key.pem
defectdojo-django-6b9547b696-tx4mk:/app$ openssl rsa -text -pubin -in /tmp/pub_key.pem
Public-Key: (2048 bit)
Modulus:
    00:b3:46:23:44:51:39:46:bd:f5:74:ab:8c:8f:0b:
    bb:05:2a:e4:da:8d:90:37:c6:29:f7:12:c7:f5:01:
    d3:db:71:f2:ca:6c:0d:e5:f7:26:4e:44:af:0a:97:
    2b:26:ed:ff:1a:e0:6a:3f:b2:ce:71:a7:17:d9:e7:
    81:9c:0d:9f:52:6c:d1:ee:53:ce:2c:9d:5e:1a:09:
    d7:9e:d8:ee:d3:42:b6:55:71:de:c8:2d:0a:e4:36:
    62:b2:d3:2f:1a:f1:c9:c8:94:29:9d:74:71:ba:d6:
    ec:39:e5:bc:b6:92:9c:d0:63:d4:17:cc:6f:b9:c2:
    5e:be:55:39:63:03:67:10:49:f5:fb:c4:91:82:a0:
    39:e7:71:94:6a:25:f7:f7:b4:17:59:4c:95:3a:52:
    bf:ad:00:7f:52:6a:32:43:23:09:64:92:72:10:b3:
    8e:fd:3c:e3:d6:f4:57:06:07:c0:0c:3f:59:58:f8:
    57:1e:d5:1a:3b:e2:6c:8d:89:f4:c5:5e:2a:65:38:
    7d:03:1d:4a:63:d6:dd:0b:c8:51:46:cd:1d:17:38:
    93:46:00:4e:a2:f1:f9:e7:96:2e:7b:5a:b8:ab:ad:
    d7:aa:f2:3e:0c:18:b3:1b:f4:e6:32:c5:3a:a5:e8:
    2c:ef:82:4f:ef:8b:b9:28:1e:bf:58:c6:02:87:16:
    4c:83
Exponent: 65537 (0x10001)
writing RSA key
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs0YjRFE5Rr31dKuMjwu7
BSrk2o2QN8Yp9xLH9QHT23HyymwN5fcmTkSvCpcrJu3/GuBqP7LOcacX2eeBnA2f
UmzR7lPOLJ1eGgnXntju00K2VXHeyC0K5DZistMvGvHJyJQpnXRxutbsOeW8tpKc
0GPUF8xvucJevlU5YwNnEEn1+8SRgqA553GUaiX397QXWUyVOlK/rQB/UmoyQyMJ
ZJJyELOO/Tzj1vRXBgfADD9ZWPhXHtUaO+JsjYn0xV4qZTh9Ax1KY9bdC8hRRs0d
FziTRgBOovH555Yue1q4q63XqvI+DBizG/TmMsU6pegs74JP74u5KB6/WMYChxZM
gwIDAQAB
-----END PUBLIC KEY-----
defectdojo-django-6b9547b696-tx4mk:/app$

Knowing that the public key is valid, I thought about validating the python library's ability to handle this information (inside the pod). So I wrote little python script:

import os
from cryptography.hazmat.primitives import serialization
import jwt.algorithms

def analyze_public_key():
    # Get environment variable
    key_content = os.getenv('DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY')
    
    if not key_content:
        print("❌ Environment variable DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY not found")
        return
    
    print("=== Keycloak Public Key Analysis ===\n")
    print(f"Content length: {len(key_content)} characters\n")
    
    # Try to identify the format
    key_format = "Unknown"
    if key_content.startswith('-----BEGIN'):
        key_format = "PEM"
    elif key_content.startswith('MII'):
        key_format = "Base64 (possible DER)"
    
    print(f"Detected format: {key_format}\n")
    print("Attempting to parse using different methods...")
    
    # Try to load as public key
    try:
        key_bytes = key_content.encode()
        public_key = serialization.load_pem_public_key(key_bytes)
        print("✓ Successfully loaded as RSA/EC public key")
        
        # Get key details
        key_size = getattr(public_key, 'key_size', None)
        if key_size:
            print(f"Key size: {key_size} bits")
            
        key_type = public_key.__class__.__name__
        print(f"Key type: {key_type}")
        
        # Try to export the key in different formats
        try:
            pem = public_key.public_bytes(
                encoding=serialization.Encoding.PEM,
                format=serialization.PublicFormat.SubjectPublicKeyInfo
            )
            print("✓ Key can be exported as PEM")
        except Exception as e:
            print(f"❌ Error exporting as PEM: {str(e)}")
            
    except Exception as e:
        print(f"❌ Error loading as public key: {str(e)}")
    
    # Try to use with PyJWT
    try:
        rsa = jwt.algorithms.RSAAlgorithm(jwt.algorithms.RSAAlgorithm.SHA256)
        rsa.prepare_key(key_content)
        print("✓ Key is accepted by PyJWT")
    except Exception as e:
        print(f"❌ Error validating with PyJWT: {str(e)}")
    
    # Print raw content for inspection
    print("\nRaw key content:")
    print("-" * 50)
    print(key_content)
    print("-" * 50)

if __name__ == "__main__":
    analyze_public_key()

save it to /tmp/check.py and run it using python3 /tmp/check.py the result was:

=== Keycloak Public Key Analysis ===

Content length: 450 characters

Detected format: PEM

Attempting to parse using different methods...
✓ Successfully loaded as RSA/EC public key
Key size: 2048 bits
Key type: RSAPublicKey
✓ Key can be exported as PEM
✓ Key is accepted by PyJWT

Raw key content:
--------------------------------------------------
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs0YjRFE5Rr31dKuMjwu7
BSrk2o2QN8Yp9xLH9QHT23HyymwN5fcmTkSvCpcrJu3/GuBqP7LOcacX2eeBnA2f
UmzR7lPOLJ1eGgnXntju00K2VXHeyC0K5DZistMvGvHJyJQpnXRxutbsOeW8tpKc
0GPUF8xvucJevlU5YwNnEEn1+8SRgqA553GUaiX397QXWUyVOlK/rQB/UmoyQyMJ
ZJJyELOO/Tzj1vRXBgfADD9ZWPhXHtUaO+JsjYn0xV4qZTh9Ax1KY9bdC8hRRs0d
FziTRgBOovH555Yue1q4q63XqvI+DBizG/TmMsU6pegs74JP74u5KB6/WMYChxZM
gwIDAQAB
-----END PUBLIC KEY-----
--------------------------------------------------

and it works too!

Steps to reproduce

Just follow the the official documentation at https://defectdojo.github.io/django-DefectDojo/integrations/social-authentication/#keycloak and take care about the step 6, because Keycloak don't add the header of footer.

Expected behavior

The DefectDojo can load the public key and finish the process of login with Keycloak.

Deployment method (select with an X)

  • Docker Compose
  • Kubernetes
  • GoDojo

Environment information

  • DefectDojo version: defectdojo/defectdojo-django:2.39.1-alpine
  • Infrastructure: AWS EKS 1.31
  • RDS Aurora Postgres: 15.4
  • Elastic Cache Redis: 7.1
  • Keycloak: 23.0.6

Logs

ERROR [django.request:241] Internal Server Error: /complete/keycloak/
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/jwt/algorithms.py", line 343, in prepare_key
    RSAPrivateKey, load_pem_private_key(key_bytes, password=None)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: ('Could not deserialize key data. The data may be in an incorrect format, the provided password may be incorrect, it may be encrypted with an unsupported algorithm, or it may be an unsupported key type (e.g. EC curves with explicit parameters).', [<OpenSSLError(code=503841036, lib=60, reason=524556, reason_text=unsupported)>])

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/jwt/algorithms.py", line 347, in prepare_key
    return cast(RSAPublicKey, load_pem_public_key(key_bytes))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: Unable to load PEM file. See https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file for more details. InvalidData(InvalidByte(0, 45))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/views/decorators/cache.py", line 80, in _view_wrapper
    response = view_func(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/views/decorators/csrf.py", line 65, in _view_wrapper
    return view_func(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/social_django/utils.py", line 49, in wrapper
    return func(request, backend, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/social_django/views.py", line 31, in complete
    return do_complete(
           ^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/social_core/actions.py", line 49, in do_complete
    user = backend.complete(user=user, redirect_name=redirect_name, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/social_core/backends/base.py", line 39, in complete
    return self.auth_complete(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/social_core/utils.py", line 253, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/social_core/backends/oauth.py", line 427, in auth_complete
    return self.do_auth(
           ^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/social_core/utils.py", line 253, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/social_core/backends/oauth.py", line 434, in do_auth
    data = self.user_data(access_token, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/social_core/backends/keycloak.py", line 134, in user_data
    return jwt.decode(
           ^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/jwt/api_jwt.py", line 211, in decode
    decoded = self.decode_complete(
              ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/jwt/api_jwt.py", line 152, in decode_complete
    decoded = api_jws.decode_complete(
              ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/jwt/api_jws.py", line 210, in decode_complete
    self._verify_signature(signing_input, header, signature, key, algorithms)
  File "/usr/local/lib/python3.11/site-packages/jwt/api_jws.py", line 314, in _verify_signature
    prepared_key = alg_obj.prepare_key(key)
                   ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/jwt/algorithms.py", line 349, in prepare_key
    raise InvalidKeyError("Could not parse the provided public key.")
jwt.exceptions.InvalidKeyError: Could not parse the provided public key.

Sample scan files

NA

Screenshots

image

@eduardovalentim I've never hooked up DefectDojo to Keycloak so don't have any advice to give beyond suggesting you ask this on the OWASP Slack instance as there's usually more traffic/conversations happening there so you're more likely to get an answer.

You can find info on getting to the OWASP Slack #defectdojo channel at https://github.com/DefectDojo/django-DefectDojo?tab=readme-ov-file#community-getting-involved-and-updates

I checked lib documentation and it looks that social_core.backends.keycloak.KeycloakOAuth2 expect PubKey in the format without -----BEGIN/END PUBLIC KEY----- prefix/suffix. You need to start with MIIBIjANBx...
https://python-social-auth.readthedocs.io/en/latest/backends/keycloak.html

Hey guys!

I apologize for the delay in testing the information shared. @kiblik, thank you so much for your help - your suggestion was spot-on and worked on the first try! @mtesauro, I appreciate you pointing me to the OWASP Slack channel.

The issue wasn't actually a bug, but rather my misinterpretation of how to properly configure the public key. As indicated in the documentation, the KeycloakOAuth2 backend expects the public key without the "-----BEGIN/END PUBLIC KEY-----" prefix/suffix, starting directly with "MIIBIjANBx...". Once I made this adjustment, everything worked perfectly.

Ironically, this was actually the first solution I tried, but for some reason (probably due to another unrelated issue), it didn't work at that time. That initial failure sent me down a rabbit hole of trial and error, leading me further away from the correct solution.

For anyone who might need it in the future, here's my working Terraform configuration using the mrparkers/keycloak provider:

provider "keycloak" {
    client_id     = var.kc_client_id
    client_secret = var.kc_client_secret
    url           = "https://${var.kc_hostname_host}"
}

data "keycloak_realm" "realm" {
    realm = var.kc_realm_name
}

data "keycloak_realm_keys" "realm_keys" {
  realm_id   = data.keycloak_realm.realm.id
  algorithms = ["RS256"]
  status     = ["ACTIVE"]
}

resource "keycloak_openid_client" "defectdojo_client" {
  realm_id              = data.keycloak_realm.realm.id
  enabled               = true
  client_id             = "defectdojo"
  name                  = "DefectDojo"
  access_type           = "CONFIDENTIAL"
  standard_flow_enabled = true
  full_scope_allowed    = false

  root_url    = "https://${var.defectdojo_host}"
  base_url    = "https://${var.defectdojo_host}"
  web_origins = ["+"]

  valid_redirect_uris = [
    "https://${var.defectdojo_host}/*"
  ]

  extra_config = {
    "user.info.response.signature.alg"    = "RS256"
    "request.object.signature.alg"        = "RS256"
  }
}

resource "keycloak_openid_audience_protocol_mapper" "aud_mapper" {
  realm_id                 = data.keycloak_realm.realm.id
  client_id                = keycloak_openid_client.defectdojo_client.id
  included_client_audience = keycloak_openid_client.defectdojo_client.client_id
  name                     = "aud"

  # Configurações adicionais
  add_to_id_token     = false
  add_to_access_token = true
}

resource "kubectl_manifest" "defectdojo_configmap" {
  yaml_body = <<YAML
apiVersion: v1
kind: ConfigMap
metadata:
  name: defectdojo-configmap
  namespace: defectdojo
data:
  ...
  DD_SESSION_COOKIE_SECURE: "true"
  DD_CSRF_COOKIE_SECURE: "true"
  DD_SECURE_SSL_REDIRECT: "true"
  DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_ENABLED: "true"
  DD_SOCIAL_AUTH_KEYCLOAK_KEY: "${keycloak_openid_client.defectdojo_client.client_id}"
  DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY: "${data.keycloak_realm_keys.realm_keys.keys.0.public_key}"
  DD_SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL: "https://${var.kc_hostname_host}/realms/${var.kc_realm_name}/protocol/openid-connect/auth"
  DD_SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL: "https://${var.kc_hostname_host}/realms/${var.kc_realm_name}/protocol/openid-connect/token"
  DD_SOCIAL_AUTH_KEYCLOAK_USER_INFO_URL: "https://${var.kc_hostname_host}/realms/${var.kc_realm_name}/protocol/openid-connect/userinfo"
  DD_SOCIAL_AUTH_KEYCLOAK_LOGOUT_URL: "https://${var.kc_hostname_host}/realms/${var.kc_realm_name}/protocol/openid-connect/logout"
  DD_SOCIAL_AUTH_KEYCLOAK_LOGIN_BUTTON_TEXT: "Login with Microsoft"
YAML

}

resource "kubectl_manifest" "defectdojo_secret" {
  yaml_body = <<-YAML
    apiVersion: v1
    kind: Secret
    metadata:
      name: defectdojo-secret
      namespace: defectdojo
    type: Opaque
    data:
      ...
      DD_SOCIAL_AUTH_KEYCLOAK_SECRET: "${base64encode(keycloak_openid_client.defectdojo_client.client_secret)}"
  YAML
}