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
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();
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();
(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");
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)