..
2026-08-18 01:08:08 -06:00
2026-08-18 01:08:08 -06:00
2026-03-17 07:49:53 -06:00
2026-08-18 01:08:08 -06:00
2026-03-17 07:49:53 -06:00
2026-03-17 07:49:53 -06:00
2026-03-17 07:49:53 -06:00
2026-03-17 07:49:53 -06:00
2026-08-18 01:08:08 -06:00
2026-03-17 07:49:53 -06:00
2026-08-18 01:08:08 -06:00
2026-08-18 01:08:08 -06:00
2026-03-17 07:49:53 -06:00

jwt

Go Reference

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.