This article kicks off a two-part, step-by-step guide on rolling out JWT-based authentication within an Angular app (though the principles scale to enterprise-level systems as well).

Our focus here is to first dig into how JSON Web Tokens (or JWTs) actually function, and how they serve User Authentication and Session Management in a web context.

Part 2 shifts to implementing JWT-based Authentication specifically in Angular, but for now, this piece zeroes in on JWTs alone.

Why a JWT Deep Dive?

Getting a thorough grasp of JWTs becomes indispensable for:

  • building a JWT-driven authentication flow from scratch
  • tackling real-world debugging—deciphering error messages and stack traces
  • vetting third-party libraries and making sense of their docs
  • crafting a custom in-house authentication mechanism
  • picking and tuning an external authentication provider

Even when you opt for a pre-built JWT-based authentication solution, some coding remains inevitable, particularly on the client side and occasionally on the backend.

By the close of this post, you'll have a solid command of JWTs, including the cryptographic foundations they rely on—concepts that show up across many other security scenarios.

You'll know exactly when and why to reach for JWTs, be able to dissect the JWT structure well enough to manually verify signatures, and have access to several online and Node-based utilities to aid that work.

Armed with those utilities, you can independently work through a host of JWT-related hiccups. So let's jump straight into the JWT deep dive!

Why JWTs?

The standout edge of JWTs (versus handling user sessions through an in-memory random token) lies in outsourcing authentication logic to an external server, which could be:

  • an internal, bespoke authentication server centralized within your org
  • commonly, a commercial system like an LDAP that can emit JWTs
  • or a fully external, third-party authentication service, e.g., Auth0

That external authentication server can operate wholly independent of your application server, with no requirement to share any secret key with other network components—meaning your application server hosts no secret key vulnerable to theft or misplacement.

Additionally, no direct, live connection between the authentication server and the application server is needed for authentication to function (more on this below).

Beyond that, the application server can be entirely stateless, since there's no need to retain tokens in-memory between requests. The authentication server can issue a token, send it off, and then discard it right away!

There's also no need to stash password digests in the application database either, which cuts down on what could be pilfered and minimizes security flaws.

Now, you might wonder: for an internal in-house app, do JWTs still make sense? Yes—we'll address leveraging JWTs in a typical enterprise Pre-Authentication scenario in the final section of this post.

Table Of Contents

Here's what we'll explore in this post:

  • What are JWTs?
  • Online tools for JWT validation
  • What is the format of a JSON Web Token
  • JWTs in a Nutshell: Header, Payload, Signature
  • Base64Url (vs Base64)
  • User Session Management with JWTs: Subject and Expiration
  • The HS256 JWT Signature - How does it work?
  • Digital Signatures
  • Hashing functions and SHA-256
  • The RS256 JWT Signature - let's talk about public key crypto
  • RS256 vs HS256 Signatures - Which one is better?
  • JWKS (JSON Web Key Set) Endpoints
  • How to implement JWT Signature Periodic Key Rotation
  • JWTs in the Enterprise
  • Summary and Conclusions

What are JWTs?

A JSON Web Token (or JWT) is essentially a JSON payload that carries a specific claim. The defining trait of JWTs is that verifying their validity hinges solely on inspecting the token itself.

To confirm the claim is legitimate, there's no need to ping an external service or store JWTs in-memory between requests—each token embeds a Message Authentication Code (or MAC, discussed later).

A JWT consists of 3 components: the Header, the Payload, and the Signature. We'll examine each, kicking off with the Payload.

What does a JWT Payload look like?

The JWT payload is merely a standard Javascript object. Below is an example of a valid payload object:

Here, the payload holds identifying details about a specific user; however, it might equally contain other data, such as transaction details for a financial transfer.

While the payload has no content constraints, it’s crucial to remember that JWTs are not encrypted. Consequently, anyone who intercepts the token can still read all the information inside it.

For this reason, avoid placing any user data in the Payload that an attacker could directly exploit.

The Role of JWT Headers - Why are they essential?

The receiver verifies the Payload’s content by checking the signature. Since multiple signature algorithms exist, the recipient must determine, for instance, which signature type it should examine.

This kind of metadata describing the token itself is stored in its own JSON structure, transmitted alongside the Payload.

This distinct JSON structure is called the JWT Header; below is an example of a legitimate header:

Clearly, it is also just a simple JSON object. Within this header, the signature algorithm for the JWT is indicated as RS256.

We’ll discuss the various signature types later; for now, let’s examine how the existence of a signature facilitates Authentication.

JWT signatures - How do they work for Authentication?

The final component of a JWT is its signature, a Message Authentication Code (or MAC). Only someone holding the payload (as well as the header) together with a specific secret key can create a JWT’s signature.

The signature is applied for Authentication in this manner:

  • the user provides their credentials to an Authentication server, which could be our server but is often a dedicated separate one
  • the Authentication server confirms the credentials, then constructs a JWT whose payload contains the user’s technical ID and an expiry time
  • following that, the Authentication server generates a signature over the Header plus Payload using a secret key and transmits the token to the user’s browser (we’ll detail the signing mechanism later)
  • the browser stores the signed JWT and attaches it to every HTTP request directed at our Application server
  • the signed JWT essentially serves as a temporary user credential, substituting the permanent one comprised of the username and password pair

Afterward, here’s the process our Application server follows with the JWT:

  • our Application server verifies the JWT signature, confirming that a key holder indeed signed this specific Payload
  • the Payload points to a particular user using a technical identifier
  • the private key exists solely on the Authentication server, which issues tokens exclusively to users who present the correct password
  • hence, our Application server can confidently conclude that the token was issued to this user by the Authentication server, thereby proving the user’s identity because they
    entered the right credentials
  • the server then handles the HTTP request as one originating from that user

An attacker could only masquerade as a user by obtaining both the username and password, or by gaining access to the signing secret held by the Authentication server.

Evidently, the signature is the most essential element of a JWT!

Thanks to the signature, a fully stateless server can verify that an HTTP request comes from a particular user simply by examining the JWT in the request itself—without demanding the password be resent each time.

Is the core purpose of JWTs to achieve a stateless server?

Statelessness is a fortunate byproduct, but the primary advantage is that the Authentication server that issues the token and the Application server that verifies it can be entirely independent systems.

Thus, minimal authentication logic is required at the application layer—verifying the JWT is enough!

An entire suite of applications could consolidate their login and registration processes at one central Authentication server.

This arrangement keeps the Applications servers more straightforward and secure, because much of the authentication work is centralized and shared across all apps.

With a high-level understanding of how JWTs support stateless third-party Authentication, it’s time to dive into the implementation specifics.

What should a JSON Web Token look like?

A video demonstrating the three structural components of a JWT, complete with code examples and a web-based validator, is available here:

Now, consider the sample JWT below, which comes from the online JWT checker at jwt.io:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

At this point, you may be wondering: what happened to the JSON objects? We will revisit them shortly. Actually, by the time you finish reading this post, you will have a thorough grasp of every element within this peculiar string.

Examining it closely: there are clearly 3 segments, each divided by a dot. The segment preceding the initial dot represents the JWT Header:

JWT Header: 
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Nestled between the first and second dots lies the second segment, known as the Payload:

JWT Payload: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

Following the second dot comes the final section, which is the Signature:

JWT Signature: 
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

To verify that the data is genuinely present, you can paste the entire JWT string into the official validation tool at jwt.io.

But what are all these seemingly random characters, and how do we decode the information back out of a JWT when we need to debug it? How does jwt.io manage to recover the JSON objects?

Base64 in a Nutshell, or is it Base64Url?

Despite appearances, the Payload, the Header, and the Signature are all still present in a human-readable form.

The reason we encode them is to avoid encountering those messy character encoding problems (like this garbled output: qîüö:Ã) when transmitting the JWT over the network.

That issue arises because systems across the globe handle text using varying character sets, such as UTF-8, ISO 8859-1, and others.

Such encoding problems are as widespread as text itself: every string in any environment relies on some encoding—even when you haven't explicitly declared one:

  • the operating system's default encoding is applied
  • or the encoding is pulled from a server configuration setting

To transmit strings without worrying about these complications, we pick a subset of characters that every common encoding interprets in the same fashion—this is exactly what led to the creation of the Base64 encoding format.

Base64 vs Base64Url

However, what appears in a JWT is technically not Base64: it is actually Base64Url.

It works like Base64, but with a few different characters, enabling easy embedding of a JWT into a Url parameter—which occurs, say, when a third-party login page redirects back to your site.

Thus, if you extract the middle section of this JWT (the part between the first and second dot), you get the Payload, displayed below:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

Try feeding it into a free online tool, such as this decoder:

The result is a JSON payload—handy to keep in mind when debugging. True, the tool was a Base64 decoder; we'll get back to that distinction shortly. For now, here's where we stand:

Summary: At this point, we have a clear picture of what lives inside a JWT Header and the Payload: they're simply two JavaScript objects, turned into JSON, then Base64Url-encoded and joined with a dot.

Essentially, this format serves as a straightforward way to ship JSON over the wire.

The accompanying video demonstrates JWT creation and validation, diving deeper into the Header and Payload sections:

Let’s step back from the signature for a moment and examine what actually goes into the Payload when we apply JWTs to user authentication in practice.

Structuring User Sessions with JWTs: Subject and Expiration

In theory, a JWT payload can hold any claims you like—user identity is just one option. However, authentication is such a frequent use case that two payload properties are specifically designed to support it:

  • identifying the user
  • setting when the session ends

Below is a sample payload demonstrating two of the most widely used JWT payload fields:

Let’s break down what each of these standard fields represents:

  • iss points to the entity that issued the token—here, it’s our authentication server
  • iat records when the JWT was created, measured in seconds from the Epoch
  • sub stores the user’s technical identifier
  • exp provides the timestamp at which the token stops being valid

This setup is what we call a Bearer Token, and it conveys a specific meaning:

The Authentication Server asserts that whoever holds this token is the user whose ID lives in the sub field—so we should let them in

With a clear picture of how the Payload operates in the standard authentication scenario, it’s time to dig into the Signature itself.

JWTs come with various signature algorithms; here, we’ll focus on two: HS256 and RS256. Let’s begin with the first one, HS256.

Inside the HS256 JWT Digital Signature - What’s Happening?

As with most signature schemes, HS256 leans on a particular kind of function known as a cryptographic hash.

That might sound daunting, but it’s a concept worth mastering: it proved valuable two decades ago and will remain crucial for years to come. Hash functions underpin a huge chunk of real-world security, appearing everywhere in web app defense.

The upside is that everything we, as web developers, need to grasp about hashing can fit into a few short explanations—and that’s exactly what’s coming up.

We’ll approach it in two stages: first, we’ll define what a hashing function is, then we’ll see how combining one with a secret key yields a Message Authentication Code (which serves as the actual digital signature).

By the time we wrap up this part, you’ll be equipped to generate the HS256 JWT signature on your own, using debugging tools found online plus an npm package.

Defining a Hashing Function

A hashing function is a distinct category of function with several unique traits, and these traits translate into many practical uses—digital signatures being a prime example.

We’ll examine four key properties of these functions, discuss why those properties enable verifiable signatures, and then put them to use.

The specific function we’re working with goes by the name SHA-256.

Hashing Function Property 1 - One-Way Trap

Think of a hashing function as a kitchen grinder: feed in whole steaks at one end, and out come hamburger patties at the other—but there’s no path backward from the patties to the original steaks:

the function cannot be reversed!

So if we run our Header and Payload through this function, nobody can reconstruct the original data simply from the resulting output.

To see what SHA-256 produces in action, test it out with this online hash calculator; you’ll get an output resembling this:

3f306b76e92c8a8fbae88a3ef1c0f9b0a81fe3a953fa9320d5d0281b059887c3

Consequently, hashing is not encryption: encryption is, by its very definition, reversible, since we must recover the original input data from the encrypted output.

Hashing Functions Property 2 - Reproducible

Hashing is also reproducible, meaning that hashing the same header and payload over and over again yields the exact same result, bit for bit.

Thus, given a set of inputs and a hash output, we can always verify that a particular output (such as a signature) is correct, because we can readily perform the same calculation ourselves—provided we have all the input components.

Hashing Functions Property 3 - No Collisions

One more interesting aspect of hashing functions is that when you feed them multiple different values, each distinct input produces its own unique output.

In practice, there are no cases where two non-identical inputs yield the same result—one unique input leads to one unique output.

This implies that using the same Payload and Header generates an identical hash output every time, and no other input data could have yielded that same output—the hash is effectively a unique fingerprint of the inputs.

Hashing Functions Property 4 - Unpredictability

One final property we review about hashing functions: given a known output value, you cannot derive its input through stepwise refinement.

For instance, take the sample hash above: if we wanted to discover which Payload generated it, we might start with an initial guess, hash it, and inspect the resulting output to gauge how close it lies to the target.

We could then adjust one character at a time and re-hash, continuing the process until the output finally matches the expected result.

Yet, there is a considerable obstacle:

With hashing functions, this approach simply won't work!

Here is why: in hashing, even a single character change in the input—indeed, a single bit flip—causes about 50 percent of the output bits to revert or differ.

So, almost any variation in the input produces an entirely different result.

All of that is intriguing, but you might wonder now: how then can a hashing function produce a digital signature? Could an attacker just take the Header and Payload and compromise the signature?

Because anyone with access to those two parts can apply SHA-256 and generate the same hash, then append it as the JWT signature, right?

How to use hashing functions to produce a signature?

It is true that everyone can recreate a Header and Payload hash using the standard procedure.

However, the HS256 signature goes further: we take the Header plus the Payload, insert a secret password into the mix, and hash this combined information.

The result is a SHA-256 HMAC, or Hash-Based Message Authentication Code: one specific function that performs this transformation is HMAC-SHA256, exactly what HS256 signatures use.

This function's output can be recreated only by someone who also holds the JWT Header and Payload (both of which are readable in any captured token) AND possesses the password.

Therefore, this two-in-one hash is an effective digital signature!

Because that hashed value serves as proof that someone with the password built and signed the Payload—it would be impossible to calculate that exact hash any other way.

Hence, the hash acts as an un-forgeable digital seal verifying the Payload's integrity.

We then attach this hash to the token itself, enabling the receiver to validate it: this kind of hash is called an HMAC (Hash-Based Message Authentication Code), that is, a digital signature type.

And that action is exactly what JWTs do: the final segment (placed after the second dot) contains the Base64Url-encoded SHA-256 hash of the Header, the Payload, and the password.

How to validate a JWT signature?

Upon receiving an HS256-signed JWT, our server must also know the same password from start to finish to authenticate the signature and verify that the token Payload indeed originates from a trusted source.

Signature verification means recalculating the hash from the JWT Header and Payload along with the password; for HS256, this equates to the receiver and sender sharing the exact same password.

If this calculation replicates the signature hash exactly, the token should be considered genuine, since only a password holder could have generated that specific signature.

This sort of process is how digital signatures and HMACs generally operate. Would you like to see a demonstration with real numbers?

Manually confirming a SHA-256 JWT Signature

If we strip away the signature from the exact JWT used earlier and drop the trailing dot as well, we end up with just the Header plus the Payload. The result would resemble this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

If you drop that same string into a web-based HMAC SHA-256 generator, such as this tool, and supply secret as the password, the output will match the JWT signature.

Well, almost — the result appears in Base64 form, trailing with an =, which differs slightly from Base64Url:

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ=

That trailing equals sign gets percent-encoded as %3D in a URL, which is a bit untidy—and it also clarifies why Base64Url exists, when the intention is to pass JWTs as URL parameters.

Online Base64Url converters are fairly scarce, yet this is something we can readily handle via the command line. To fully validate the HS256 signature, here's an npm package offering Base64Url support, including bidirectional conversion with Base64.

The base64url NPM Package

Next, we'll apply it to turn our output into Base64 URL form, thereby confirming the signature and cementing exactly how the mechanism works:

mkdir quick-test && cd quick-test
npm init
npm install base64url
 
node
> const base64url = require('base64url');
> base64url.fromBase64("TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ=")

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

And here we are, at last — this string is the HS256 JWT signature we set out to recreate:

It matches the JWT above exactly, down to the very last character!

So congratulations are in order: you now have a thorough grasp of how HS256 JWT signatures operate, and you'll be able to debug your way through any challenge with these online tools and packages.

So why are there other signature types?

That, in a nutshell, is how JWT signatures support Authentication, with HS256 serving as just one illustration of a signature type. Yet other signature types exist, and the most widely adopted one is RS256.

What sets them apart? We've focused on HS256 here primarily because it makes the concept of a MAC code far easier to grasp, and you could easily encounter it in real-world applications.

Still, it's far preferable to adopt something like the RS256 signature method instead, since, as the next section will show, it offers a host of benefits over HS256.

Drawbacks of HS256 Signatures

If the input secret is weak, HS256 signatures become vulnerable to brute force, though the same criticism applies to many other key-based systems.

Yet hash-based signatures are notably more susceptible to brute force compared to other options, especially with typical production key lengths.

More importantly, a key practical flaw of HS256 is the need for a pre-arranged secret password shared between the server issuing the JWTs and every other server machine that consumes those JWTs for validation and user identification.

Cumbersome Key Rotation

Consequently, switching the password means distributing and installing it across all network nodes that rely on it — a process that's inconvenient, prone to mistakes, and likely to require synchronized server downtime.

This might even prove impossible if, say, one server is operated by an entirely different team or even a third-party organization.

No split between token creation and validation

The core issue is the lack of separation between the ability to mint JWTs and the ability to merely verify them: with HS256, anyone on the network can both generate and validate tokens because they all possess the secret password.

This widens the attack surface, as the password must be deployed everywhere, and not every application maintains the same level of security operations.

A partial workaround is to assign a separate shared password per application, but instead, we'll explore a new signature method that resolves all these issues and has become the standard for modern JWT-based solutions: RS256.

THe RS256 JWT Signature

With RS256, we still aim to generate a Message Authentication Code as before, with the goal of creating a digital signature that verifies a JWT's authenticity.

However, this signature method separates the ability to create valid tokens—reserved exclusively for the Authentication server—from the ability to verify JWT tokens, which is what the Application server needs.

To achieve that, we will generate two keys rather than just one:

  • A private key remains, but this time it's held solely by the Authentication server and is used only to sign JWTs
  • That private key can sign JWTs, yet it cannot be used to validate them
  • A second key, called the public key, is used by the application server exclusively to verify JWTs
  • The public key can confirm JWT signatures, but it cannot create new JWTs
  • The public key doesn't need to stay confidential and frequently isn't, since possession of it offers no way to forge signatures

Introducing The RSA encryption technology

RS256 signatures rely on a specific key type called RSA Keys. RSA is an encryption/decryption algorithm that uses one key to encrypt and a different key to decrypt.

Keep in mind that RSA is not a Hashing function, since encryption's output can be reversed, recovering the original data.

Here's an example of what an RSA Public Key looks like:

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB
-----END PUBLIC KEY-----  

Once more, this may appear intimidating, but it is simply a special value produced via a command-line utility such as openssl or a web-based RSA key generator like this example.

And this key might be exposed, and in practice it is commonly shared, so a malicious party has no need to figure it out: it is typically already available.

However, a matching RSA Private key also exists:

-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQABAoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5CpuGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0KSu5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aPFaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw==
-----END RSA PRIVATE KEY-----  

Here's the good news: an attacker has no realistic way of guessing that secret!

Let’s recap—the two keys are paired together: whatever one encrypts, only the other can decrypt. So how can this pairing be turned into a signature?

Why not just encrypt the payload with RSA?

One straightforward idea for a digital signature would be this: take the Header and Payload, encrypt them as a block with the RSA private key, and ship that as the JWT.

On the receiving side, the JWT is decrypted using the Public Key, and the output is inspected.

If decryption succeeds and the result resembles a JSON payload, it can only be the Authentication Server that produced and encrypted this data. So the token must be valid, correct?

That logic holds up—this approach would indeed prove the token's authenticity. Yet we don't implement it this way, for practical reasons.

RSA encryption is notably slower than, say, a hashing algorithm. When payloads grow larger, this slowdown becomes a real concern—and that's just the first issue.

So what's the actual approach? How do HS256 signatures put RSA to work in real life?

Using RSA and SHA-256 to sign a JWT (RSA-SHA256)

In practice, we first run the Header and Payload through a hash function like SHA-256.

Hashing is fast, and it yields a compact, unique fingerprint of the input—far smaller than the data itself.

Next, we take that hash digest and encrypt it with the RSA private key, instead of encrypting the entire header-and-payload block. This encrypted digest is the RS256 signature!

Finally, we attach it as the third segment of the JWT and transmit the whole thing.

How does the receiver check RS256 Signatures?

On the receiving end, the JWT validator performs these steps:

  • hash the header and payload together using SHA-256
  • decrypt the incoming signature with the public key to recover the original hash
  • compare that recovered hash to the one they just computed from the header and payload

If the two hashes line up, it's proof that the JWT originated from the Authentication server!

Any party could have recalculated that hash, but only the Authentication server holds the private key capable of encrypting it correctly.

Feels like there must be more to it, right? Let's verify that instinct and pick up troubleshooting techniques for RS256 signatures along the way.

Manually confirming an RS256 JWT signature

We'll begin with a sample JWT signed via RS256, taken from jwt.io:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE

At first glance, this token looks identical to the HS256 example — yet it was actually signed using the RSA Private Key from earlier. Let’s strip away the signature and focus solely on the Header and the Payload:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

To finalize the JWT, we now apply a SHA-256 hash to this data and then use the RSA private key described earlier to encrypt it.

This yields the JWT signature. Let's verify this using Node's built-in Crypto module—no extra installation is required since this functionality ships with Node.

The module includes a built-in RSA-SHA256 function, among other signing methods, which we can leverage to recreate the signature.

First, we save the RSA private key into a text file called private.key.

Next, from the command line, we launch the Node shell and run this small snippet:

Should you employ a JWT different from the test one used here, only copy the two initial segments into the write call, leaving out the JWT signature.

The output we obtain is this:

 EkN+DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W/A4K8ZPJijNLis4EZsHeY559a4DFOd50/OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k/4zM3O+vtd1Ghyo4IbqKKSy6J9mTniYJPenn5+HIirE=

The signature works in a totally different way, mind you. However, note the slashes and equal signs in there: you could not drop this into a URL without extra encoding steps.

The reason is that we encoded the signature using Base64, not Base64Url. So converting it goes like this:

bash$ node
const base64url = require('base64url');
base64url.fromBase64("EkN+DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W/A4K8ZPJijNLis4EZsHeY559a4DFOd50/OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k/4zM3O+vtd1Ghyo4IbqKKSy6J9mTniYJPenn5+HIirE=");

And here is what get back:

EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE 

That is exactly the RS256 signature we intended to generate, bit for bit!

This confirms our grasp of RS256 JWT signatures, and we now have the knowledge to debug them when issues arise.

To sum up, RS256 JWT signatures are nothing more than an RSA-encrypted SHA-256 hash derived from the header and payload.

At this point we understand how RS256 signatures function, but what makes them superior to HS256?

RS256 Signatures vs HS256 - Why use RS256?

In the case of RS256, the attacker can readily carry out the initial phase of the signature process: computing the SHA-256 hash from the values of a pilfered JWT header and payload.

However, to go from there and reconstruct a signature, he would need to crack RSA, which for a sufficiently large key size is impractical.

Yet for most applications, that is not the most compelling reason to opt for RS256 over HS256.

Additionally, with RS256, we have the certainty that the private key capable of signing tokens resides solely on the Authentication server, where it enjoys far greater protection — so RS256 reduces the risk of the signing private key being compromised.

But an even larger practical benefit of choosing RS256 exists: streamlined key rotation.

How to implement key rotation

Recall that the public key used for token validation can be published anywhere, and the attacker effectively cannot exploit it in any meaningful way.

After all, what advantage would an attacker gain from validating a stolen JWT? An attacker seeks to forge JWTs, not to verify them.

This opens the door to publishing the public key on a server we manage.

The application servers then merely need to connect to that server to obtain the public key, and periodically re-check it in case it has changed, whether due to an urgent rotation or a scheduled one.

As a result, there is no requirement to take both the application server and the authentication server offline simultaneously and update the keys everywhere at once.

But in what format can the public key be published? Here is one possible approach.

JWKS (JSON Web Key Set) Endpoints

Several formats exist for publishing public keys, but one that will seem familiar is JWKS, short for JSON Web Key Set.

Plenty of straightforward npm packages are available for consuming these endpoints and validating JWTs, as we will cover in part two.

These endpoints can serve multiple public keys, not just a single one.

Should you be curious about what such endpoints look like, check out this live example — here is what comes back from an HTTP GET request:

The kid property functions as the key identifier, while x5c represents one specific public key.

The main advantage of this format is that it is standard, so we only need the endpoint URL plus a library that supports JWKS — this provides us a ready-to-use public key for JWT validation without requiring installation on our server.

JWTs are frequently linked with public internet sites and social login systems. But what about intranet and in-house applications?

JWTs in the Enterprise

JWTs also fit well in the enterprise setting, serving as a strong alternative to the typical Pre-Authentication configuration, which poses a known security risk.

In the Pre-Authentication model used by many organizations, we host our application servers behind a proxy on a private network and simply read the current user from an HTTP header.

The HTTP header identifying the user is typically populated by a centralized network component, often a login page hosted on a proxy server that manages the user session.

That server denies access to the application if the session has expired, and it authenticates users following login.

It then forwards all requests to the application server, tacking on an HTTP header to designate the user.

Here lies the issue: with this setup, virtually anyone on the internal network can impersonate a user just by adding that same HTTP header!

Mitigations exist, such as IP allow-listing for the proxy server at the application server level or using a client certificate, but in reality, most companies lack these safeguards.

A better version of the Pre-Authentication Setup

The Pre-Authentication approach has a clear upside: application developers are relieved of implementing authentication themselves in every app, which saves effort and reduces the chance of introducing security flaws.

What if we could keep the ease of Pre-Authentication without weakening security—avoiding a setup where authentication in our app is trivially bypassed, even when confined to a private network?

Bringing JWT into the mix makes this straightforward: rather than placing the username directly in the header as with typical Pre-Authentication, we turn that HTTP header into a JWT.

Next, the username goes into the payload of the token, which the Authentication server signs.

The Application server, instead of naively reading the username from the header, checks the JWT first:

  • if the signature turns out valid, the user is deemed properly authenticated and the request proceeds
  • otherwise, the application server simply refuses the request

Consequently, authentication now works solidly, even within the private network!

There’s no longer a need to put unconditional trust in an HTTP header bearing a username. Instead, we can verify that the header truly originates from the proxy and is legitimate—not an attacker posing as someone else.

Conclusions

This post provided a broad look at JWTs and their role in Authentication. At their core, JWTs are JSON payloads equipped with a signature that is both easy to verify and impossible to forge.

Keep in mind, JWTs themselves have nothing inherently tied to authentication—they can carry any kind of claim over the network.

A frequent security application of JWTs is Authorization: for instance, we might place a user’s authorization roles—like Read Only User or Administrator—directly into the Payload.

The upcoming installment in this series will cover implementing Authentication in an Angular app with JWTs.

I hope you found this useful; if you have any questions or comments, feel free to drop them below and I’ll respond.

For a deeper dive into securing Angular applications, we suggest the Angular Security Course, which explores JWTs in far greater depth.

Sign up for our newsletter to stay informed when new posts like this are published:

New to Angular? Check out the Angular for Beginners Course:

JWT: The Complete Guide to JSON Web Tokens — figure 1

The JWT Handbook by Auth0

Navigating RS256 and JWKS

Brute Forcing HS256 is Possible: The Importance of Using Strong Keys in Signing JWTs

JSON Web Key Set (JWKS)

Beyond these, you might want to explore other well-received posts on the blog: