Skip to content
ecdsa.com
Go

golang-jwt/jwt

golang-jwt/jwt, the community-maintained successor of dgrijalva/jwt-go and the most used JWT package for Go: key-typed verification, none behind an explicit sentinel, and optional expiry unless you ask for it.
License
MIT
Checked version
5.3.1 ·
Source
github.com/golang-jwt/jwt
Documentation
pkg.go.dev/github.com/golang-jwt/jwt/v5

Compatibility at a glance

JOSE and JWT

JWS algorithms
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSA

github.com/golang-jwt/jwt/v5 parses, validates and signs JWTs with the keys from the standard library: []byte for HMAC, *rsa.PublicKey, *ecdsa.PublicKey and ed25519.PublicKey. 5.3.1 signed and verified the HS, RS, PS and ES families and EdDSA; ES256K and the fully specified Ed25519 and Ed448 names have no registered signing method.

Verification hinges on the keyfunc you pass to jwt.Parse. Each signing method checks the Go type of the key it receives, so with a keyfunc that returns *ecdsa.PublicKey, an RS256 token failed with RSA verify expects *rsa.PublicKey and an HS256 token keyed with the PEM text failed with HMAC verify expects []byte. The protection disappears if the keyfunc hands back raw bytes for every token — a keyfunc that loads the PEM file as []byte let that forged HS256 token verify in the check. Pass jwt.WithValidMethods with the exact algorithm, and keep the keyfunc typed, as the sample does.

alg: none verifies only when the keyfunc returns the sentinel jwt.UnsafeAllowNoneSignatureType; any real key yields 'none' signature type is not allowed. ES256 refuses a P-384 key at signing time with a bare key is invalid. ES256 signatures are the 64-byte raw r‖s that JWS requires; a DER signature fails with crypto/ecdsa: verification error, which the JWT debugger and the DER ⇄ raw converter help track down.

Claims validation is on by default in v5, but exp is optional — a token without it is valid forever — and there is no leeway. jwt.WithExpirationRequired(), jwt.WithIssuer, jwt.WithAudience and jwt.WithLeeway close those gaps; errors can be matched with errors.Is(err, jwt.ErrTokenExpired) instead of comparing strings.

To verify tokens from an identity provider, pair it with a JWKS client and check what the provider publishes with the JWKS checker; go-jose is the alternative when JWE or JWK handling is needed.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/golang-jwt/run.sh ran golang-jwt/jwt 5.3.1 in the container image golang:1.27.3-trixie on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • JWS algorithms

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSA

    HS, RS, PS and ES at 256/384/512 bits and EdDSA signed and verified; ES256K, Ed25519 and Ed448 have no registered signing method.

    Recorded by the check
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, EdDSA
    evidence jws-algorithms: ES256K: no signing method registered
    evidence jws-algorithms: Ed25519: no signing method registered
    evidence jws-algorithms: Ed448: no signing method registered
  • ES256 signature encoding

    raw

    ES256 signatures are 64 bytes of raw r‖s.

    Recorded by the check
    fact jws-ecdsa-encoding = raw
  • DER signature inside a JWS

    rejected

    A DER-encoded ES256 signature fails with token signature is invalid: crypto/ecdsa: verification error.

    Recorded by the check
    fact jws-der-signature = rejected
    evidence jws-der-signature: token signature is invalid: crypto/ecdsa: verification error
  • Algorithm allow-list

    from-key

    Without WithValidMethods, the key's Go type decides: an RS256 token against a keyfunc returning an EC key failed with key is of invalid type: RSA verify expects *rsa.PublicKey.

    Recorded by the check
    fact jwt-algorithm-allowlist = from-key
    evidence jwt-algorithm-allowlist: RS256 token, keyfunc returning the EC public key, no WithValidMethods: token signature is invalid: key is of invalid type: RSA verify expects *rsa.PublicKey
  • Unsigned tokens (alg none)

    opt-in

    An unsigned token fails with 'none' signature type is not allowed unless the keyfunc returns jwt.UnsafeAllowNoneSignatureType.

    Recorded by the check
    fact jwt-alg-none = opt-in
    evidence jwt-alg-none: with the EC key: token signature is invalid: token is unverifiable: 'none' signature type is not allowed
    evidence jwt-alg-none: keyfunc returning jwt.UnsafeAllowNoneSignatureType: <nil>
  • Public key used as an HMAC secret

    rejected

    An HS256 token keyed with the PEM text failed against a keyfunc returning *ecdsa.PublicKey (HMAC verify expects []byte), but verified when the keyfunc returned the PEM bytes.

    Recorded by the check
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: keyfunc returning *ecdsa.PublicKey: token signature is invalid: key is of invalid type: HMAC verify expects []byte
    evidence jwt-hmac-public-key: keyfunc returning the PEM bytes (a keyfunc that loads keys as bytes): <nil>
  • Curve checked against alg

    enforced

    Signing ES256 with a P-384 key fails with key is invalid.

    Recorded by the check
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: key is invalid
  • Default clock skew

    0s

    No leeway by default: a token that expired three seconds earlier was rejected.

    Recorded by the check
    fact jwt-clock-skew = 0s
  • Expiry claim required

    no

    A token without exp was valid; WithExpirationRequired() makes it fail with token is missing required claim: exp claim is required.

    Recorded by the check
    fact jwt-exp-required = no
    evidence jwt-exp-required: with WithExpirationRequired: token has invalid claims: token is missing required claim: exp claim is required

Usage

Sign registered claims with ES256 and parse them back with a typed keyfunc, the method pinned and issuer, expiry and leeway enforced.
package main

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"fmt"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

func main() {
	key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	claims := jwt.RegisteredClaims{Subject: "user-42", Issuer: "https://auth.example.com", ExpiresAt: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}
	signed, _ := jwt.NewWithClaims(jwt.SigningMethodES256, claims).SignedString(key)

	// The keyfunc returns a typed key; the options pin the method and the claims.
	token, err := jwt.ParseWithClaims(signed, &jwt.RegisteredClaims{},
		func(*jwt.Token) (any, error) { return &key.PublicKey, nil },
		jwt.WithValidMethods([]string{"ES256"}),
		jwt.WithIssuer("https://auth.example.com"),
		jwt.WithExpirationRequired(),
		jwt.WithLeeway(30*time.Second),
	)
	fmt.Println("valid:", err == nil && token.Valid, "sub:", token.Claims.(*jwt.RegisteredClaims).Subject)
}

The snippet harness runs every sample verbatim in the container image golang:1.27.3-trixie, and the run fails unless the output matches.

Command
GOFLAGS=-mod=mod go run .
Output must match (regular expression)
valid: true sub: user-42

Libraries