Archive - April 2025 Back

Troubleshooting Client Assertion in OAuth 2.0: JWT Signature Pitfalls

Posted on 17/4/2025 under tech

JWT Client Assertions: more about it here

In OAuth 2.0, client assertion is a secure and efficient method for client authentication. The client generates a JWT (JSON Web Token), signs it with its private key, and sends it to the server as proof of identity. The server then authenticates the client using the corresponding public key.

This is typically done using asymmetric algorithms like ES256 (ECDSA with P-256 and SHA-256), which are favored for their security advantages over symmetric alternatives.

However, when putting this concept into practice, many developers (myself included) hit a few frustrating roadblocks. One of the most common issues? The signature just won’t validate, whether you’re testing in jwt.io or in your own Python script using cryptography.

Let’s break down the key reasons why signature verification might fail — and how to fix them:

 

1. The Private Key Must Be in PKCS#8 Format

After generating your EC private key with OpenSSL, you must convert it to PKCS#8 format. Most libraries, including Python's cryptography, expect this format.

openssl pkcs8 -topk8 -nocrypt -in ec_private_key.pem -out private_key_pkcs8.pem

 

2. Generate the Public Key from the PKCS#8 Private Key

openssl ec -in private_key_pkcs8.pem -pubout -out public.pem

 

3. The JWT Signature Must Be in Raw (r || s) Format, Not DER

This one catches almost everyone.

The JWT/JWS spec (RFC 7515) requires ECDSA signatures in concatenated raw format (r || s, 64 bytes total). But by default, most libraries (including Python’s cryptography) return DER-encoded signatures — and that breaks validation in tools like jwt.io or with compliant verifiers.

If you're manually generating the JWT, you must convert the DER signature to raw format. Example below in Python:

 

from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature

# Sign and convert DER → raw (r || s)
der_signature = private_key.sign(signing_input, ec.ECDSA(hashes.SHA256()))
r, s = decode_dss_signature(der_signature)
r_bytes = r.to_bytes(32, byteorder="big")
s_bytes = s.to_bytes(32, byteorder="big")
raw_signature = r_bytes + s_bytes

 

💡 Real-World Gotchas

I've run into these exact issues working with enterprise platforms like SuccessFactors and Microsoft Power Automate. In many cases, key format requirements aren't explicitly documented, which leads to time-consuming trial and error — especially when you're dealing with black-box integrations.

 

🔐 Final Thoughts

Handling cryptographic keys is a critical part of secure authentication — and one that’s often overlooked until it breaks something. I hope this post saves you the same headaches I ran into. Now go forth and sign your tokens with confidence!

Have fun playing with crypto keys — they’re frustrating until they’re fun. 😄

I'm now an author in Medium

Posted on 20/4/2025 under medium

I start documenting my tech insights & lessons learn with Medium. 

Do follow me there -> Dylan Lee - Medium

 

I'm glad that back then when I developed this blog, I did developed the function to update my links.