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

algKey requiredTypical error when mismatched
HS256Secret ≥ 32 bytes“Key must be at least 256 bits” (Nimbus, .NET)
RS256 / PS256RSA private key ≥ 2048 bits“Key is not an RSA private key”
ES256EC 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

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