jwt
Lightweight JWT/JWS/JWK library for JOSE, OIDC, and OAuth 2.1.
Asymmetric (Ed25519, ECDSA, RSA) and symmetric (HMAC) signing.
Go 1.26+.
- Issuer: sign tokens, publish JWKS
- Relying Party: verify signatures, validate claims
- Key management: file-based loading, remote JWKS fetching, graceful rotation
- HMAC for legacy systems
Issuer: Sign and Issue Tokens
Create a signer from private keys, then issue tokens with any claims type.
package main
import (
"fmt"
"os"
"time"
"github.com/therootcompany/golib/auth/jwt"
"github.com/therootcompany/golib/auth/jwt/keyfile"
)
func main() {
privKey, err := keyfile.LoadPrivatePEM("signing-key.pem")
if err != nil {
panic(err)
}
signer, err := jwt.NewSigner([]*jwt.PrivateKey{privKey})
if err != nil {
panic(err)
}
// Publish the JWKS endpoint: json.Marshal(&signer.WellKnownJWKs)
claims := &jwt.TokenClaims{
Iss: "https://auth.example.com",
Sub: "user-42",
Aud: jwt.Listish{"https://api.example.com"},
Exp: time.Now().Add(time.Hour).Unix(),
IAt: time.Now().Unix(),
}
token, err := signer.SignToString(claims)
if err != nil {
panic(err)
}
fmt.Println(token)
}
Relying Party: Verify and Validate
Verify the signature, then validate claims (exp, aud, iss, etc.).
package main
import (
"fmt"
"os"
"time"
"github.com/therootcompany/golib/auth/jwt"
"github.com/therootcompany/golib/auth/jwt/keyfile"
)
func main() {
pubKey, err := keyfile.LoadPublicPEM("signing-key.pub")
if err != nil {
panic(err)
}
verifier, err := jwt.NewVerifier([]jwt.PublicKey{*pubKey})
if err != nil {
panic(err)
}
jws, err := verifier.VerifyJWT("eyJhbGciOiJFZERTQSIs...")
if err != nil {
switch err {
case jwt.ErrUnknownKID:
fmt.Println("Unknown signing key")
case jwt.ErrSignatureInvalid:
fmt.Println("Invalid signature")
default:
panic(err)
}
os.Exit(1)
}
var claims jwt.TokenClaims
if err := jws.UnmarshalClaims(&claims); err != nil {
panic(err)
}
validator := jwt.NewAccessTokenValidator(
[]string{"https://auth.example.com"}, // issuers
[]string{"https://api.example.com"}, // audiences
)
if err := validator.Validate(nil, &claims, time.Now()); err != nil {
fmt.Printf("Claims invalid: %v\n", err)
os.Exit(1)
}
fmt.Printf("Valid token for %s\n", claims.Sub)
}
Key Management
For asymmetric keys, KID is auto-computed from the RFC 7638 thumbprint by default.
Load Keys from Files
privKey, err := keyfile.LoadPrivatePEM("key.pem")
pubKey, err := keyfile.LoadPublicPEM("key.pub")
privJWK, err := keyfile.LoadPrivateJWK("key.jwk")
pubJWK, err := keyfile.LoadPublicJWK("key.pub.jwk")
jwks, err := keyfile.LoadWellKnownJWKs("keys.jwks.json")
Fetch Keys from Remote JWKS Endpoint
fetcher, err := keyfetch.NewKeyFetcher("https://auth.example.com/.well-known/jwks.json")
verifier := fetcher.Verifier() // verifies with live remote keys
Retired Keys
Pass retired public keys to NewSigner so they appear in the JWKS endpoint and can verify tokens signed before key rotation:
oldPubKey, _ := keyfile.LoadPublicPEM("old-signing-key.pub")
newPrivKey, _ := keyfile.LoadPrivatePEM("new-signing-key.pem")
signer, err := jwt.NewSigner([]*jwt.PrivateKey{newPrivKey}, oldPubKey)
The retired key is never used for signing but appears in signer.WellKnownJWKs and can verify legacy tokens.
HMAC: Legacy Signing
HMAC-signed JWTs for backwards compatibility. HS256, HS384, HS512.
package main
import (
"crypto"
"os"
"time"
"github.com/therootcompany/golib/auth/jwt"
)
func main() {
secret := []byte(os.Getenv("JWT_SECRET"))
key := jwt.NewSharedSecret(secret, "optional-key-id")
signer, err := jwt.NewHMACSigner(crypto.SHA256, key)
if err != nil {
panic(err)
}
token, err := signer.SignToString(&jwt.TokenClaims{
Iss: "legacy.example.com",
Sub: "user-42",
Exp: time.Now().Add(time.Hour).Unix(),
})
if err != nil {
panic(err)
}
verifier, err := jwt.NewHMACVerifier(crypto.SHA256, key)
if err != nil {
panic(err)
}
jws, err := verifier.VerifyJWT(token)
if err != nil {
panic(err)
}
var claims jwt.TokenClaims
jws.UnmarshalClaims(&claims)
println(claims.Sub) // "user-42"
}
Shared Secret from Environment
export JWT_SECRET=$(openssl rand -base64 32)
secret := []byte(os.Getenv("JWT_SECRET"))
ss := jwt.NewSharedSecret(secret, "my-key")
signer, err := jwt.NewHMACSigner(crypto.SHA256, ss)
Load Secret from File
// Raw file contents as secret (KID left unset)
key, err := keyfile.LoadSharedSecretString("secret.txt")
// JWK format (same as asymmetric keys)
key, err := keyfile.LoadSharedSecretJWK("secret.jwk")
Retired Secrets
For key rotation, pass retired secrets to verify tokens signed with older keys:
active := jwt.NewSharedSecret(activeSecret, "active")
retired := jwt.NewSharedSecret(oldSecret, "retired")
verifier, err := jwt.NewHMACVerifier(crypto.SHA256, active, retired)
The verifier tries the active secret first, then retired secrets. The first successful verification wins.
Key ID (KID)
For HMAC keys, KID is left unset — you set it explicitly.
key := jwt.NewSharedSecret(secret, "")
// key.KID is empty
// To set KID from the RFC 7638 thumbprint:
key.KID, _ = key.Thumbprint()
It's best to leave the Key ID kid unset for HMAC secrets unless you have multiple secrets and you know whether using thumbprint or opaque ids is right for your use case.