DEV Community

Guillermo Contreras
Guillermo Contreras

Posted on

Encrypt your JWTs in .NET, not just sign them

A signed JWT (JWS) is tamper-proof, but it's not private — paste one into
jwt.io and you'll read every claim. If your token carries an email, a role or any
PII, you want it encrypted (JWE). Here's how, with
Matios.Security — dependency-free JOSE/JWT for
.NET, MIT.

Install

dotnet add package Matios.Security
Enter fullscreen mode Exit fullscreen mode

First, a normal signed token (JWS)

using Matios.Security.Jose;
using Matios.Security.Jwt;

var signingKey = SymmetricJoseKey.FromBase64Url(signingKeyB64, "sig-2026");

string token = new JwtBuilder()
    .Issuer("my-platform")
    .Subject(userId)
    .Lifetime(TimeSpan.FromMinutes(30))
    .Claim("role", "admin")
    .SignWith(signingKey)
    .Create();
Enter fullscreen mode Exit fullscreen mode

Integrity: ✅. Confidentiality: ❌ — the payload is just base64, readable by anyone
who intercepts it.

Now encrypt it — one extra line

Add .EncryptWith(...) and the same builder produces a Nested JWT (signed
inside, encrypted outside). The token becomes opaque — dir + A256GCM:

string token = new JwtBuilder()
    .Subject(userId)
    .Lifetime(TimeSpan.FromMinutes(30))
    .Claim("role", "admin")
    .Claim("email", "ana@acme.cl")
    .SignWith(signingKey)
    .EncryptWith(encryptionKey)      // ← now the claims are hidden
    .Create();
Enter fullscreen mode Exit fullscreen mode

(Fun fact: Microsoft's own stack can't even issue a JWE with A256GCM — this
library does, cross-platform.)

Validate — strict by design

JwtClaims claims = JwtValidator.Validate(token, new JwtValidationParameters
{
    SigningKey    = signingKey,
    DecryptionKey = encryptionKey,   // set ⇒ a plain JWS is rejected (no downgrade)
    ValidIssuer   = "my-platform"
});

string role = claims.GetClaim<string>("role");
Enter fullscreen mode Exit fullscreen mode

You declare the accepted alg/enc — no alg:"none", no algorithm confusion,
crit/zip rejected. Validation failures raise one generic error; the real reason
stays in a log-only failure code (anti-oracle).

Docs and examples → https://security.matios.cl. Part of a
family of zero-dependency, MIT packages for .NET — matios.cl.

If you try it, I'd love your feedback — issues and stars welcome. 🙌

Top comments (0)