When I’m working on an API that validates bearer tokens, I rarely want to set up the whole identity provider just to call one endpoint. For local development and integration tests, a token I sign myself with the same shared secret is enough. It only has to be signed exactly the way the API expects — and that’s where most attempts go wrong.
This guide walks through building one with the JWT generator, which signs in your browser and never stores or shares the secret.
What’s in a JWT
A JWT is three Base64URL strings joined by dots: header, payload, signature.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzQyIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzYwMDAwMDAwLCJleHAiOjE3NjAwMDM2MDB9.dHTnh_w9yth3K0cDYQ3BZSs7ZYARJstimmGEZ3kvarQ
The header is {"alg":"HS256","typ":"JWT"}. The payload holds the claims:
{"sub":"user_42","role":"admin","iat":1760000000,"exp":1760003600}
The signature is HMAC-SHA256 over header.payload, using the secret. Anyone can decode the first two parts — the JWT decoder does it instantly — so never put secrets in claims. The signature only proves that someone with the key created the token.
Claims APIs usually check
exp— expiry. Most APIs reject tokens without one. The generator warns if it’s missing, in the past, or earlier thaniat.iat— issued at.nbf— not before.issandaud— issuer and audience. ASP.NET Core’s JwtBearer validates both by default, so they must matchValidIssuerandValidAudienceexactly, including trailing slashes.sub, roles or scopes — whatever your authorization policies read.
The generator has buttons that set iat to now, exp to 15 minutes, an hour, a day or a week from now, nbf to now, and jti to a random UUID.
Seconds, not milliseconds
iat, exp and nbf are NumericDate values: seconds since 1970. Date.now() in JavaScript returns milliseconds, and a token with "exp": 1760003600000 expires in the year 57,700 — or, depending on the library, fails validation as a malformed date. The generator flags any 13-digit value.
The secret: encoding and length
This is the part that causes most “signature is invalid” errors.
Encoding. A secret is bytes. Some frameworks take the configured string as UTF-8 text; others treat it as Base64 and decode it first. The same characters produce completely different key bytes and therefore a different signature. If your server’s configuration looks like dGhpcy1pcy1vbmx5LWEtdGVzdC1zZWNyZXQtb2YtMzJi, it’s probably Base64 — set “Secret is” to Base64 and paste it as is. In .NET, check whether the key is built with Encoding.UTF8.GetBytes(secret) or Convert.FromBase64String(secret).
Length. RFC 7518 requires an HMAC key at least as long as the hash: 32 bytes for HS256, 48 for HS384, 64 for HS512. Microsoft.IdentityModel and the jose library reject shorter keys outright, with errors about key size rather than signatures. The generator warns before you get that far.
Checking the result
“Open in JWT decoder” sends the token — not the secret — to the decoder through the URL fragment, which browsers never send to a server. There you can see the decoded claims and verify the HMAC signature with the secret. The generator’s output is tested against tokens from the jsonwebtoken npm package, so a mismatch at this point means the server expects a different secret, encoding or algorithm.
Then call the API:
curl -H "Authorization: Bearer <token>" https://localhost:5001/api/orders
If you need that call in C#, paste the curl command into the cURL converter.
About alg: none
The JWT spec allows an unsigned token with "alg": "none". Early libraries accepted these even when a signature was expected, which let attackers forge tokens by stripping the signature. Every modern library rejects none unless explicitly configured, and you should test that yours does. The generator will create one only after you tick an explicit checkbox, with a warning, for exactly that test.
What this is not for
A self-signed HS256 token is for development and tests. Production tokens should come from your identity provider, usually signed with RS256 or ES256 so APIs only need the public key. For those, the JWT decoder can verify signatures against a JWK. And for HMAC outside JWTs — webhook signatures in particular — see the HMAC generator.