33import hmac
44import logging
55import ssl
6- from typing import Any , Dict
6+ from typing import Any , Dict , List
77
88import jwt
99from cryptography import x509
1313
1414from app .config import ConfigClientOAuth
1515from app .models .ura import UraNumber
16- from app .utils .certificates .utils import enforce_cert_newlines
1716
1817SSL_CLIENT_CERT_HEADER_NAME = "x-forwarded-tls-client-cert" # "x-proxy-ssl_client_cert"
1918
@@ -34,14 +33,22 @@ def _create_ssl_context(self) -> ssl.SSLContext:
3433 """
3534 Create an SSL context for mTLS connections to the JWKS endpoint.
3635 """
37- if self .config .mtls_cert is None or self .config .mtls_key is None or self .config .verify_ca is None :
38- raise ValueError ("mTLS certificate and key must be provided for Client OAuth2" )
36+ if (
37+ self .config .mtls_cert is None
38+ or self .config .mtls_key is None
39+ or self .config .verify_ca is None
40+ ):
41+ raise ValueError (
42+ "mTLS certificate and key must be provided for Client OAuth2"
43+ )
3944
4045 context = ssl .create_default_context ()
4146 if isinstance (self .config .verify_ca , bool ) and self .config .verify_ca is True :
4247 context .verify_mode = ssl .CERT_REQUIRED
4348
44- context .load_cert_chain (certfile = self .config .mtls_cert , keyfile = self .config .mtls_key )
49+ context .load_cert_chain (
50+ certfile = self .config .mtls_cert , keyfile = self .config .mtls_key
51+ )
4552 if isinstance (self .config .verify_ca , str ):
4653 context .load_verify_locations (cafile = self .config .verify_ca )
4754
@@ -65,7 +72,9 @@ def verify(self, request: Request) -> Dict[str, Any]:
6572 """
6673 auth_header = request .headers .get ("authorization" )
6774 if not auth_header or not auth_header .lower ().startswith ("bearer " ):
68- raise HTTPException (status_code = 401 , detail = "Missing or invalid Authorization header" )
75+ raise HTTPException (
76+ status_code = 401 , detail = "Missing or invalid Authorization header"
77+ )
6978
7079 token = auth_header [7 :] # Remove "Bearer "
7180 claims = self ._verify_token (token )
@@ -77,7 +86,9 @@ def _verify_token(self, token: str) -> Dict[str, Any]:
7786 """
7887 Verify JWT token and return claims.
7988 """
80- jwk_client = PyJWKClient (self .config .jwks_url , cache_keys = True , ssl_context = self ._ssl_context )
89+ jwk_client = PyJWKClient (
90+ self .config .jwks_url , cache_keys = True , ssl_context = self ._ssl_context
91+ )
8192 signing_key = jwk_client .get_signing_key_from_jwt (token ).key
8293
8394 try :
@@ -105,16 +116,16 @@ def _verify_mtls(request: Request, claims: Dict[str, Any]) -> None:
105116 Verify mTLS binding by checking cnf.x5t#S256 against presented client certificate thumbprint.
106117 """
107118
108- # Extract presented client certificate thumbprint from request
109- cert_pem = request .headers .get (SSL_CLIENT_CERT_HEADER_NAME )
110- if not cert_pem :
119+ certs = ClientOAuthService .get_pem_from_request (request )
120+ if not certs :
111121 logger .error ("Client certificate not presented or verification failed" )
112- raise HTTPException (status_code = 401 , detail = "Client certificate not presented or verification failed" )
113-
114- cert_pem = enforce_cert_newlines (cert_pem )
122+ raise HTTPException (
123+ status_code = 401 ,
124+ detail = "Client certificate not presented or verification failed" ,
125+ )
115126
116- # Calculate thumbprint of presented certificate
117- presented_cert = x509 .load_pem_x509_certificate (cert_pem .encode ())
127+ # Calculate thumbprint of presented client certificate, ignoring the chain
128+ presented_cert = x509 .load_pem_x509_certificate (certs [ 0 ] .encode ())
118129 cert_der = presented_cert .public_bytes (serialization .Encoding .DER )
119130 sha256_hash = hashlib .sha256 (cert_der ).digest ()
120131 request_cert_fp = base64 .urlsafe_b64encode (sha256_hash ).rstrip (b"=" ).decode ()
@@ -135,3 +146,38 @@ def _verify_mtls(request: Request, claims: Dict[str, Any]) -> None:
135146 if not hmac .compare_digest (fp , request_cert_fp ):
136147 logger .debug ("mTLS binding failed" )
137148 raise HTTPException (status_code = 401 , detail = "Invalid token" )
149+
150+ @staticmethod
151+ def get_pem_from_request (request : Request ) -> List [str ]:
152+ """
153+ Extracts and returns the PEM-encoded client certificate from the request headers.
154+ """
155+ if (
156+ SSL_CLIENT_CERT_HEADER_NAME not in request .headers
157+ or not request .headers .get (SSL_CLIENT_CERT_HEADER_NAME )
158+ ):
159+ logger .debug ("Client certificate not found or verification failed." )
160+ return []
161+
162+ certs = request .headers .get (SSL_CLIENT_CERT_HEADER_NAME , "" ).split ("," )
163+ return [
164+ ClientOAuthService .fixup_cert_headers_and_footers (cert ) for cert in certs
165+ ]
166+
167+ @staticmethod
168+ def fixup_cert_headers_and_footers (cert : str ) -> str :
169+ # Add PEM headers/footers if missing
170+ if not cert .startswith ("-----BEGIN CERTIFICATE-----" ):
171+ cert = (
172+ "-----BEGIN CERTIFICATE-----\n " + cert + "\n -----END CERTIFICATE-----"
173+ )
174+
175+ # If we are by any chance missing newlines after/before the headers, add them
176+ if not cert .startswith ("-----BEGIN CERTIFICATE-----\n " ):
177+ cert = cert .replace (
178+ "-----BEGIN CERTIFICATE-----" , "-----BEGIN CERTIFICATE-----\n "
179+ )
180+ cert = cert .replace (
181+ "-----END CERTIFICATE-----" , "\n -----END CERTIFICATE-----"
182+ )
183+ return cert
0 commit comments