Skip to content
ecdsa.com

Sign an ES256 JWT · Go

How to sign a JWT with ES256 in Go

To sign a JWT with ES256 in Go, golang-jwt/jwt v5 builds the token from RegisteredClaims and signs it with an *ecdsa.PrivateKey parsed from PEM. The library writes the 64-byte JWS signature for you; the run checks the token with OpenSSL against the public key.

Tested with github.com/golang-jwt/jwt/v5 v5.3.1 (Go 1.27.3).

main.go — ES256 with golang-jwt/jwt v5
package main
import (
"fmt"
"log"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
)
func main() {
pemBytes, err := os.ReadFile("key.pem")
if err != nil {
log.Fatal(err)
}
privateKey, err := jwt.ParseECPrivateKeyFromPEM(pemBytes) // PKCS#8 or SEC 1 ("EC PRIVATE KEY")
if err != nil {
log.Fatal(err)
}
now := time.Now()
claims := jwt.RegisteredClaims{
Issuer: "https://issuer.example.com",
Subject: "user-42",
Audience: jwt.ClaimStrings{"api://orders"},
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(15 * time.Minute)),
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
token.Header["kid"] = "2026-10"
signed, err := token.SignedString(privateKey) // *ecdsa.PrivateKey, not the PEM bytes
if err != nil {
log.Fatal(err)
}
fmt.Println(signed)
}

How it works

  1. jwt.ParseECPrivateKeyFromPEM reads PKCS#8 and SEC 1 (EC PRIVATE KEY) blocks and returns *ecdsa.PrivateKey.
  2. jwt.RegisteredClaims covers iss, sub, aud, iat and exp; jwt.NewNumericDate truncates times to whole seconds.
  3. token.Header["kid"] adds the key id; NewWithClaims has already set alg and typ.
  4. SignedString(privateKey) returns the compact token header.payload.signature.

Gotchas

  • SignedString takes the parsed key, not PEM bytes: passing []byte returns key is of invalid type: ECDSA sign expects *ecdsa.PrivateKey.
  • golang-jwt writes a single audience as an array, "aud":["api://orders"]. RFC 7519 allows both forms; if a strict consumer insists on a string, set jwt.MarshalSingleStringAsArray = false.
  • A P-384 key with SigningMethodES256 fails with the terse key is invalid. Use SigningMethodES384 for P-384 keys.
  • Custom claims belong in a struct that embeds jwt.RegisteredClaims, not in jwt.MapClaims with hand-made time values: the struct keeps types and time precision consistent.

Related recipes