How to sign a JWT
Signing correctly means matching the algorithm to the key, loading the key in the right format, setting kid, and verifying your own output. Here is the practical detail, with ES256 code in seven languages.
Match algorithm and key
| alg | Key required | Typical error when mismatched |
|---|---|---|
| HS256 | Secret ≥ 32 bytes | “Key must be at least 256 bits” (Nimbus, .NET) |
| RS256 / PS256 | RSA private key ≥ 2048 bits | “Key is not an RSA private key” |
| ES256 | EC private key on P-256 | “Curve mismatch” when using a P-384 key |
The algorithm must be chosen explicitly, never inferred from the key. JWTEncoder follows the same rule: if the key doesn't fit the selected algorithm, you get a specific message such as “The selected ES256 algorithm requires an EC key compatible with P-256.”
Key formats
- PKCS#8 PEM (
BEGIN PRIVATE KEY) — the most portable format for RSA and EC private keys. - PKCS#1 (
BEGIN RSA PRIVATE KEY) and SEC1 (BEGIN EC PRIVATE KEY) — older OpenSSL formats; convert withopenssl pkcs8 -topk8 -nocrypt. - JWK — JSON keys with
dfor private material; convenient in JavaScript and for JWKS publishing.
Setting kid
Put a key ID in the header whenever verifiers might hold more than one key. A good kid is stable and unique; the RFC 7638 thumbprint is a standards-based choice that anyone can recompute from the public key.
Verify what you sign
In tests, verify each token you issue with the public key your verifiers will use. It catches wrong keys, mismatched curves and DER-encoded ECDSA signatures early. JWTEncoder does this automatically for every token.
// After signing (jose)
await jwtVerify(token, publicKey, { algorithms: ["ES256"] });Code in seven languages
JavaScript
import { SignJWT, importPKCS8 } from "jose";
const key = await importPKCS8(process.env.JWT_PRIVATE_KEY_PEM, "ES256");
const now = Math.floor(Date.now() / 1000);
const payload = {
iss: "https://auth.example.com/",
sub: "user-123",
aud: "api.example.com",
iat: now,
exp: now + 900,
};
const token = await new SignJWT(payload)
.setProtectedHeader({
alg: "ES256",
typ: "JWT",
kid: "2026-09-p256",
})
.sign(key);
console.log(token);
TypeScript
import { SignJWT, importPKCS8, type JWTPayload } from "jose";
const key = await importPKCS8(process.env.JWT_PRIVATE_KEY_PEM!, "ES256");
const now = Math.floor(Date.now() / 1000);
const payload: JWTPayload = {
iss: "https://auth.example.com/",
sub: "user-123",
aud: "api.example.com",
iat: now,
exp: now + 900,
};
const token = await new SignJWT(payload)
.setProtectedHeader({
alg: "ES256",
typ: "JWT",
kid: "2026-09-p256",
})
.sign(key);
console.log(token);
Python
import os
import time
import jwt # pip install "pyjwt[crypto]"
key = os.environ["JWT_PRIVATE_KEY_PEM"]
now = int(time.time())
payload = {
"iss": "https://auth.example.com/",
"sub": "user-123",
"aud": "api.example.com",
"iat": now,
"exp": now + 900,
}
token = jwt.encode(
payload,
key,
algorithm="ES256",
headers={
"kid": "2026-09-p256",
},
)
print(token)
Java
// com.nimbusds:nimbus-jose-jwt:10.x
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;
import java.security.*;
import java.security.interfaces.ECPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.*;
public class Sign {
public static void main(String[] args) throws Exception {
String pem = System.getenv("JWT_PRIVATE_KEY_PEM")
.replaceAll("-----(BEGIN|END) PRIVATE KEY-----", "").replaceAll("\\s", "");
PrivateKey privateKey = KeyFactory.getInstance("EC")
.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(pem)));
JWSSigner signer = new ECDSASigner((ECPrivateKey) privateKey);
long now = System.currentTimeMillis() / 1000L;
JWTClaimsSet claims = new JWTClaimsSet.Builder()
.issuer("https://auth.example.com/")
.subject("user-123")
.audience("api.example.com")
.issueTime(new Date((now) * 1000L))
.expirationTime(new Date((now + 900) * 1000L))
.build();
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
.type(new JOSEObjectType("JWT"))
.keyID("2026-09-p256")
.build();
SignedJWT jwt = new SignedJWT(header, claims);
jwt.sign(signer);
System.out.println(jwt.serialize());
}
}
Go
package main
import (
"fmt"
"log"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
)
func main() {
key, err := jwt.ParseECPrivateKeyFromPEM([]byte(os.Getenv("JWT_PRIVATE_KEY_PEM")))
if err != nil {
log.Fatal(err)
}
now := time.Now().Unix()
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
"iss": "https://auth.example.com/",
"sub": "user-123",
"aud": "api.example.com",
"iat": now,
"exp": now + 900,
})
token.Header["kid"] = "2026-09-p256"
signed, err := token.SignedString(key)
if err != nil {
log.Fatal(err)
}
fmt.Println(signed)
}
C#
// dotnet add package Microsoft.IdentityModel.JsonWebTokens
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;
var ecdsa = ECDsa.Create();
ecdsa.ImportFromPem(Environment.GetEnvironmentVariable("JWT_PRIVATE_KEY_PEM"));
var key = new ECDsaSecurityKey(ecdsa);
key.KeyId = "2026-09-p256";
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var claims = new Dictionary<string, object?>
{
["iss"] = "https://auth.example.com/",
["sub"] = "user-123",
["aud"] = "api.example.com",
["iat"] = now,
["exp"] = now + 900,
};
var descriptor = new SecurityTokenDescriptor
{
Claims = claims,
SigningCredentials = new SigningCredentials(key, "ES256"),
};
// Do not add exp/iat/nbf automatically — use exactly the claims above.
var handler = new JsonWebTokenHandler { SetDefaultTimesOnTokenCreation = false };
Console.WriteLine(handler.CreateToken(descriptor));
PHP
<?php
// composer require firebase/php-jwt
require 'vendor/autoload.php';
use Firebase\JWT\JWT;
$key = getenv('JWT_PRIVATE_KEY_PEM');
$now = time();
$payload = [
'iss' => 'https://auth.example.com/',
'sub' => 'user-123',
'aud' => 'api.example.com',
'iat' => $now,
'exp' => $now + 900,
];
$token = JWT::encode($payload, $key, 'ES256', '2026-09-p256');
echo $token . PHP_EOL;
Generate a P-256 key pair and sign in the browser, then copy the code above pre-filled with your claims.
Sign an ES256 token