Post

JOSE 101 [1/2]: A Practical Guide to JWT, JWA, JWS and JWE in .NET

JOSE 101 [1/2]: A Practical Guide to JWT, JWA, JWS and JWE in .NET

Here we go again, this time on a topic that trips up even seasoned developers, with the hope of making it simpler. While most articles I’ve seen on the internet stop at “sign a token with HS256 and move on”, I’m going further in this post with a more comprehensive guide to grasp all the important concepts, alongside the practical JOSE_101 .NET playground app you can use to test everything shown here.

By the end of the post, you’ll be able to understand how to sign tokens with symmetric and asymmetric keys, encrypt payloads to make them unreadable (not just signed), and even combine both by nesting a signed token inside an encrypted one.

As you advance through the topics, complexity grows, and you’ll come away not just knowing what JWS and JWE stand for, but understanding which one to pick, based on your needs and considering the trade-offs.

⚠️ Disclaimer: This series and the app cover a curated set of algorithms picked to teach the concepts (HS256, RS256, ES256 for signing; dir and RSA-OAEP for key management; A256GCM for content), not every JOSE variant.

If you’re already well versed in the JWS concepts, you can jump straight to JOSE 101 (2/2), where I finish the series with JWE.


The J squad


Before diving into JOSE, it’s worth starting with the format everything here is built on: JSON (JavaScript Object Notation), the default way systems exchange structured data, readable by humans, easy to parse, and supported natively or via libraries in effectively every language. It’s simple by design: just objects, arrays, strings, numbers, booleans, and null.

That simplicity is exactly why JOSE was built around it. When the industry needed a standard way to sign and encrypt small chunks of data, such as tokens, claims and credentials, JSON fit because it carries enough structure to be meaningful, while staying simple enough to serialize compactly and parse anywhere.

Every concept in this post is a JSON object with rules attached, determining what a token contains, which algorithms are allowed to protect it, and how signing or encryption is applied on top.

JOSE (JSON Object Signing and Encryption)

JOSE isn’t an implementation, but a specification, more precisely, a family of RFCs (Request for Comments) that define exactly how those “rules attached to a JSON object” work. It’s not about what your data should be, but how to protect it.

Since it’s platform-agnostic, there are plenty of implementations across multiple stacks and languages. In this post, I’ll stick to jose-jwt, a popular .NET implementation I used in the console app.

JWT (JSON Web Token)

JWT is the format most people often refer to as ‘token’, usually in the authentication context, largely used in APIs and distributed systems. It’s a compact, URL-safe way to represent a set of claims, but on its own, it says nothing about data protection, and it’s where the other Js below come in.

Technically, a JOSE token is built from two JSON objects. The header describes how the token is protected, where alg defines the algorithm used, and typ its media type:

1
{ "alg": "HS256", "typ": "JWT" }

And the payload carries the claims, the actual data you want to transport:

Both get base64url-encoded and joined with dots into a long string. How many parts come after those two is what tells whether it’s a JWS or a JWE.

⚠️ Note: the header isn’t something you write by hand. The library builds it for you at signing time, filling alg with whichever algorithm you asked for and typ with JWT. All you hand it is the payload.

JWA (JSON Web Algorithms)

JWA is the registry of algorithm identifiers JOSE tokens are allowed to use, and it covers two different jobs.

For signing, there’s a single choice: the alg header names the signing algorithm (HS256, RS256, ES256). For encryption, there are two: alg names the key management algorithm (dir, RSA-OAEP), and enc names the content encryption algorithm (A256GCM).

Keeping those two apart is worth the effort, because they’re picked independently, and it’s the pairing that determines a token’s security properties.

JWS (JSON Web Signature)

JWS is technically a signed JWT. The payload stays fully readable, base64url-encoded but not encrypted, and a signature proves it hasn’t been tampered with. It only lets you verify who signed it and that it wasn’t altered, but anyone can read it. In practice, when you use JWTs for production-grade applications, you’re (hopefully) using a JWS, because you must prove authenticity.

JWE (JSON Web Encryption)

JWE is an encrypted JWT. The payload becomes opaque ciphertext, unreadable without the right key. It guarantees that only the intended recipient can read it, since the content is hidden from anyone else. While JWS only proves authenticity, JWE hides content.


JWS - Signing algorithms: HMAC vs RSA vs ECDSA


Symmetric or Asymmetric?

Before signing anything, you need to pick an algorithm, and that choice comes down to one key question: do the signer and the verifier trust each other with the same secret, or not?

That’s the split between symmetric and asymmetric algorithms, and it changes the whole trust model around the key, not just the math behind it.

A symmetric algorithm uses one secret for both signing and verifying: whoever holds it can produce a valid signature and check one. That’s simple and fast, but it also means every verifier is a potential signer, which implies both parties holding that secret must trust each other.

An asymmetric algorithm splits that power in two: a private key signs, a public key verifies. In practice, anyone can check a signature’s authenticity without ever being able to forge one, and that’s exactly what you want when tokens travel to services you don’t fully trust with signing power.

The three segments

A JWS has three parts, joined by dots: header.payload.signature.

  • The header declares the alg.
  • The payload carries the claims.
  • The signature is what proves the other two weren’t touched on the way.

Let’s play with a variety of tokens using the app.

First, clone JOSE_101, run it and choose this menu option: Verify / Decrypt (validate a token).

Then paste the token and the secret key to verify it. You can repeat the same process for all tokens below, while choosing the right menu option for each.

This will allow us to decompose the token into segments, and validate its integrity. That’s worth sitting with for a second, because it’s the distinction the whole post rests on: signing protects those bytes, it never hides them.

⚠️ Keys and certificates are published in the repo so anyone can reproduce the examples. Never reuse them to protect anything real!

HMAC (HS256) - Symmetric

HMAC (Hash-based Message Authentication Code) mixes a secret key with a hash function, a one-way algorithm that turns any input into a fixed-size fingerprint, so the same input always produces the same fingerprint, but you can’t work backwards from the fingerprint to the input. That combination produces a code that proves both integrity (nothing changed) and authenticity (only someone holding the secret could have produced it).

HMAC-SHA256 pairs HMAC with SHA-256 (Secure Hash Algorithm 2), a hash function whose fingerprint is always 256 bits long, no matter the size of the input. Together they give the alg value you’ll see in the token header: HS256.

When to use it: Think a single backend signing its own session tokens, or a CI pipeline signing build artifacts that only its own deploy step needs to verify.

Trade-off: it can’t prove who signed a token to a third party, and that means anyone holding the secret could have produced it. If that secret ever leaks, forged tokens become indistinguishable from real ones.

Token example:

Signature secret key:

Implementation:

RSA (RS256) - Asymmetric

RSA, named after its inventors Rivest, Shamir and Adleman, is an asymmetric algorithm whose security relies on how hard it is to factor the product of two large prime numbers. That combination lets the private key sign while the public key verifies, so proving a signature never requires the power to produce one.

RSASSA-PKCS1-v1_5 pairs RSA with SHA-256 for hashing. Together they give the alg value you’ll see in the token header: RS256.

When to use it: unlike HS256 above, it fits scenarios where the verifier shouldn’t be able to forge tokens. Think a token handed to multiple downstream services, or a public key published as a JWKS endpoint.

Trade-off: it costs more CPU than HMAC and needs key-pair management, rotation, distribution of the public key. That adds complexity and governance, but it’s the price for scaling verification out to parties you don’t fully trust with signing power. It also produces the largest signature of the three (four times ECDSA’s), which rides on every request.

💡 One detail worth knowing before you commit to it: PKCS#1 v1.5 is the older of the two padding schemes RSA signatures can use. It’s still everywhere and still considered safe for signing, but the modern choice is RSA-PSS, which adds randomness to the padding and appears in JOSE as PS256. If you’re picking RSA for a new system and both ends support it, prefer PS256.

Token example:

RSA Public key:

Implementation:

ECDSA (ES256) - Asymmetric

ECDSA (Elliptic Curve Digital Signature Algorithm) is also asymmetric, but its security comes from the math of elliptic curves instead of factoring large numbers, which is what lets it reach the same security level as RSA with much smaller keys and signatures, and cheaper signing.

ECDSA pairs with curve P-256 and SHA-256 for hashing. Together they give the alg value you’ll see in the token header: ES256.

When to use it: it fits the same scenarios as RS256, but pays less for them. Think a high-traffic API where every request carries a token, or mobile and embedded clients where both bandwidth and CPU are scarce.

Trade-off: every signature needs a high-quality random nonce (handled internally by .NET). Reuse one and you leak the private key, a concern RSA doesn’t have.

⚠️ That’s not a theoretical risk: Sony shipped the PlayStation 3 signing with the same nonce every single time, and some smart folks used exactly that to recover the console’s private signing key.

💡 For new systems without legacy RSA constraints, it’s generally the better default.

Token example:

EC Public key:

Implementation:

Same claims, three signatures

Since the first two segments are the same length across all three (the alg value differs but is always five characters), every size difference you can see comes from the signature alone. The table makes the trade-off easy to spot:

algSignatureTokenKey used
HS25643 chars121 charsone shared 256-bit secret
ES25686 chars164 charsEC P-256 key pair
RS256342 chars420 charsRSA 2048 key pair

Notice that RS256 produces a signature four times larger than ES256 at a comparable security level, and that cost rides along on every single request carrying the token. And the payload sits there in plain sight in all three, base64url-encoded and easily readable. Signing never hides anything, and that’s precisely the gap JWE exists to fill.

💡 Rule of thumb:

  • Use HMAC when signer and verifier are the same trusted system.
  • Use RSA or ECDSA when they’re not, preferring ECDSA unless you need RSA for compatibility reasons.


Final thoughts


In this post, I walked you through JWS concepts and the practical implementation in .NET. I hope this gave you a quick tour through the theory, enough to reason about these decisions in real projects.

Choosing the right implementation is not only a technical decision but, more importantly, a business one, and it demands weighing the trade-offs.

With JWS covered, we’re ready to explore JWE in the next post. See you there!



Check the project on GitHub



This post is licensed under CC BY 4.0 by the author.