mirror of
https://github.com/therootcompany/golib.git
synced 2026-11-04 15:05:00 +00:00
248 lines
5.8 KiB
Markdown
248 lines
5.8 KiB
Markdown
# jwt
|
|
|
|
[](https://pkg.go.dev/github.com/therootcompany/golib/auth/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.
|
|
|
|
```go
|
|
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.).
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
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
|
|
|
|
```sh
|
|
export JWT_SECRET=$(openssl rand -base64 32)
|
|
```
|
|
|
|
```go
|
|
secret := []byte(os.Getenv("JWT_SECRET"))
|
|
ss := jwt.NewSharedSecret(secret, "my-key")
|
|
signer, err := jwt.NewHMACSigner(crypto.SHA256, ss)
|
|
```
|
|
|
|
### Load Secret from File
|
|
|
|
```go
|
|
// 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:
|
|
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
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.
|