golib/auth/jwt/tests/round-trip-go-jose/round_trip_test.go

661 lines
19 KiB
Go

// Package josert_test verifies interoperability between this library and
// github.com/go-jose/go-jose/v4 (JWS, JWK, JWT). It covers sign/verify,
// JWK serialization, thumbprint consistency, JWKS, audience, custom claims,
// NumericDate precision, and stress tests.
package josert_test
import (
"crypto"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"testing"
"time"
jose "github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"github.com/therootcompany/golib/auth/jwt"
"github.com/therootcompany/golib/auth/jwt/tests/testkeys"
)
var longTests = flag.Bool("long", false, "run extended stress tests (100 RSA iterations instead of 10)")
// joseAlg maps our algorithm name to a go-jose SignatureAlgorithm constant.
func joseAlg(name string) jose.SignatureAlgorithm {
switch name {
case "EdDSA":
return jose.EdDSA
case "ES256":
return jose.ES256
case "ES384":
return jose.ES384
case "ES512":
return jose.ES512
case "RS256":
return jose.RS256
}
panic("unknown alg: " + name)
}
// --- helpers ---
func assertOurSignGoJoseVerify(t *testing.T, ks testkeys.KeySet, sub string) {
t.Helper()
signer, err := jwt.NewSigner([]*jwt.PrivateKey{ks.PrivKey})
if err != nil {
t.Fatalf("NewSigner: %v", err)
}
tokenStr, err := signer.SignToString(testkeys.TestClaims(sub))
if err != nil {
t.Fatalf("SignToString: %v", err)
}
// Parse and verify with go-jose.
tok, err := josejwt.ParseSigned(tokenStr, []jose.SignatureAlgorithm{joseAlg(ks.AlgName)})
if err != nil {
t.Fatalf("go-jose ParseSigned: %v", err)
}
var claims josejwt.Claims
if err := tok.Claims(ks.RawPub, &claims); err != nil {
t.Fatalf("go-jose Claims: %v", err)
}
if claims.Subject != sub {
t.Errorf("sub: got %q, want %q", claims.Subject, sub)
}
if claims.Issuer != "https://example.com" {
t.Errorf("iss: got %q, want %q", claims.Issuer, "https://example.com")
}
}
func assertGoJoseSignOurVerify(t *testing.T, ks testkeys.KeySet, sub string) {
t.Helper()
// Use JSONWebKey wrapper to get kid in the JWS header.
sigKey := jose.SigningKey{
Algorithm: joseAlg(ks.AlgName),
Key: jose.JSONWebKey{
Key: ks.RawPriv,
KeyID: ks.KID,
},
}
joseSigner, err := jose.NewSigner(sigKey, nil)
if err != nil {
t.Fatalf("go-jose NewSigner: %v", err)
}
now := time.Now()
claims := josejwt.Claims{
Issuer: "https://example.com",
Subject: sub,
Expiry: josejwt.NewNumericDate(now.Add(time.Hour)),
IssuedAt: josejwt.NewNumericDate(now),
}
tokenStr, err := josejwt.Signed(joseSigner).Claims(claims).Serialize()
if err != nil {
t.Fatalf("go-jose Serialize: %v", err)
}
verifier, _ := jwt.NewVerifier([]jwt.PublicKey{ks.PubKey})
verifiedJWS, err := verifier.VerifyJWT(tokenStr)
if err != nil {
t.Fatalf("our verify: %v", err)
}
var decoded jwt.TokenClaims
if err := verifiedJWS.UnmarshalClaims(&decoded); err != nil {
t.Fatalf("UnmarshalClaims: %v", err)
}
if decoded.Sub != sub {
t.Errorf("sub: got %q, want %q", decoded.Sub, sub)
}
}
// --- Our sign, go-jose verify (all algorithms) ---
func TestOurSignGoJoseVerify_EdDSA(t *testing.T) {
assertOurSignGoJoseVerify(t, testkeys.GenerateEdDSA("k1"), "user-eddsa")
}
func TestOurSignGoJoseVerify_ES256(t *testing.T) {
assertOurSignGoJoseVerify(t, testkeys.GenerateES256("k1"), "user-es256")
}
func TestOurSignGoJoseVerify_ES384(t *testing.T) {
assertOurSignGoJoseVerify(t, testkeys.GenerateES384("k1"), "user-es384")
}
func TestOurSignGoJoseVerify_ES512(t *testing.T) {
assertOurSignGoJoseVerify(t, testkeys.GenerateES512("k1"), "user-es512")
}
func TestOurSignGoJoseVerify_RS256(t *testing.T) {
assertOurSignGoJoseVerify(t, testkeys.GenerateRS256("k1"), "user-rs256")
}
// --- go-jose sign, our verify (all algorithms) ---
func TestGoJoseSignOurVerify_EdDSA(t *testing.T) {
assertGoJoseSignOurVerify(t, testkeys.GenerateEdDSA("k1"), "user-eddsa")
}
func TestGoJoseSignOurVerify_ES256(t *testing.T) {
assertGoJoseSignOurVerify(t, testkeys.GenerateES256("k1"), "user-es256")
}
func TestGoJoseSignOurVerify_ES384(t *testing.T) {
assertGoJoseSignOurVerify(t, testkeys.GenerateES384("k1"), "user-es384")
}
func TestGoJoseSignOurVerify_ES512(t *testing.T) {
assertGoJoseSignOurVerify(t, testkeys.GenerateES512("k1"), "user-es512")
}
func TestGoJoseSignOurVerify_RS256(t *testing.T) {
assertGoJoseSignOurVerify(t, testkeys.GenerateRS256("k1"), "user-rs256")
}
// --- JWK serialization interop ---
func TestJWKInterop_OurJSONToGoJose(t *testing.T) {
for _, ag := range testkeys.AllAlgorithms() {
t.Run(ag.Name+"_Public", func(t *testing.T) {
ks := ag.Generate("jwk-" + ag.Name)
// Marshal our public key to JSON.
ourJSON, err := json.Marshal(ks.PubKey)
if err != nil {
t.Fatalf("marshal our pubkey: %v", err)
}
// Parse with go-jose.
var joseKey jose.JSONWebKey
if err := json.Unmarshal(ourJSON, &joseKey); err != nil {
t.Fatalf("go-jose unmarshal from our JSON: %v", err)
}
// Verify a token signed by us, using the go-jose-parsed key.
signer, err := jwt.NewSigner([]*jwt.PrivateKey{ks.PrivKey})
if err != nil {
t.Fatal(err)
}
tokenStr, err := signer.SignToString(testkeys.TestClaims("jwk-interop"))
if err != nil {
t.Fatal(err)
}
tok, err := josejwt.ParseSigned(tokenStr, []jose.SignatureAlgorithm{joseAlg(ks.AlgName)})
if err != nil {
t.Fatal(err)
}
var claims josejwt.Claims
if err := tok.Claims(joseKey.Key, &claims); err != nil {
t.Fatalf("go-jose verify with our-JSON-parsed key: %v", err)
}
})
t.Run(ag.Name+"_Private", func(t *testing.T) {
ks := ag.Generate("jwk-priv-" + ag.Name)
// Marshal our private key to JSON.
ourJSON, err := json.Marshal(ks.PrivKey)
if err != nil {
t.Fatalf("marshal our privkey: %v", err)
}
// Parse with go-jose.
var joseKey jose.JSONWebKey
if err := json.Unmarshal(ourJSON, &joseKey); err != nil {
t.Fatalf("go-jose unmarshal from our private JSON: %v", err)
}
// Sign with the go-jose-parsed key, verify with our lib.
joseKey.KeyID = ks.KID
sigKey := jose.SigningKey{
Algorithm: joseAlg(ks.AlgName),
Key: joseKey,
}
joseSigner, err := jose.NewSigner(sigKey, nil)
if err != nil {
t.Fatal(err)
}
claims := josejwt.Claims{
Subject: "jwk-priv-interop",
Expiry: josejwt.NewNumericDate(time.Now().Add(time.Hour)),
}
tokenStr, err := josejwt.Signed(joseSigner).Claims(claims).Serialize()
if err != nil {
t.Fatalf("go-jose sign with our-JSON-parsed key: %v", err)
}
verifier, _ := jwt.NewVerifier([]jwt.PublicKey{ks.PubKey})
if _, err := verifier.VerifyJWT(tokenStr); err != nil {
t.Fatalf("our verify: %v", err)
}
})
}
}
func TestJWKInterop_GoJoseJSONToOur(t *testing.T) {
for _, ag := range testkeys.AllAlgorithms() {
t.Run(ag.Name, func(t *testing.T) {
ks := ag.Generate("jose-to-our-" + ag.Name)
// Create go-jose JWK and serialize.
joseKey := jose.JSONWebKey{
Key: ks.RawPub,
KeyID: ks.KID,
Algorithm: ks.AlgName,
Use: "sig",
}
joseJSON, err := json.Marshal(joseKey)
if err != nil {
t.Fatalf("marshal go-jose key: %v", err)
}
// Parse with our library.
var recovered jwt.PublicKey
if err := json.Unmarshal(joseJSON, &recovered); err != nil {
t.Fatalf("our unmarshal of go-jose JSON: %v", err)
}
// Sign with our signer, verify with the recovered key.
signer, err := jwt.NewSigner([]*jwt.PrivateKey{ks.PrivKey})
if err != nil {
t.Fatal(err)
}
tokenStr, err := signer.SignToString(testkeys.TestClaims("jose-json"))
if err != nil {
t.Fatal(err)
}
verifier, _ := jwt.NewVerifier([]jwt.PublicKey{recovered})
if _, err := verifier.VerifyJWT(tokenStr); err != nil {
t.Fatalf("verify with go-jose-JSON-parsed key: %v", err)
}
})
}
}
// --- Thumbprint consistency (RFC 7638) ---
func TestThumbprintConsistency_GoJose(t *testing.T) {
for _, ag := range testkeys.AllAlgorithms() {
t.Run(ag.Name, func(t *testing.T) {
ks := ag.Generate("thumb-" + ag.Name)
// Our thumbprint (returns base64url string).
ourThumb, err := ks.PubKey.Thumbprint()
if err != nil {
t.Fatalf("our Thumbprint: %v", err)
}
// go-jose thumbprint (returns raw bytes).
joseKey := jose.JSONWebKey{Key: ks.RawPub}
joseRaw, err := joseKey.Thumbprint(crypto.SHA256)
if err != nil {
t.Fatalf("go-jose Thumbprint: %v", err)
}
joseThumb := base64.RawURLEncoding.EncodeToString(joseRaw)
if ourThumb != joseThumb {
t.Errorf("thumbprint mismatch:\n ours: %s\n go-jose: %s", ourThumb, joseThumb)
}
})
}
}
// --- JWKS interop ---
func TestJWKSInterop_OurToGoJose(t *testing.T) {
// Build a signer with all 5 key types.
var keys []*jwt.PrivateKey
var sets []testkeys.KeySet
for _, ag := range testkeys.AllAlgorithms() {
ks := ag.Generate("jwks-" + ag.Name)
keys = append(keys, ks.PrivKey)
sets = append(sets, ks)
}
signer, err := jwt.NewSigner(keys)
if err != nil {
t.Fatal(err)
}
// Serialize our JWKS.
jwksData, err := json.Marshal(&signer)
if err != nil {
t.Fatal(err)
}
// Parse with go-jose.
var joseJWKS jose.JSONWebKeySet
if err := json.Unmarshal(jwksData, &joseJWKS); err != nil {
t.Fatalf("go-jose unmarshal JWKS: %v", err)
}
if len(joseJWKS.Keys) != 5 {
t.Fatalf("expected 5 keys, got %d", len(joseJWKS.Keys))
}
// Sign tokens with each key and verify with the go-jose-parsed set.
for i, ks := range sets {
tokenStr, err := signer.SignToString(testkeys.TestClaims(fmt.Sprintf("jwks-%d", i)))
if err != nil {
t.Fatalf("sign[%d]: %v", i, err)
}
tok, err := josejwt.ParseSigned(tokenStr, []jose.SignatureAlgorithm{joseAlg(ks.AlgName)})
if err != nil {
t.Errorf("parse[%d] (%s): %v", i, ks.AlgName, err)
continue
}
// Find the matching key from the parsed JWKS.
matching := joseJWKS.Key(ks.KID)
if len(matching) == 0 {
t.Errorf("no key found for kid %q", ks.KID)
continue
}
var claims josejwt.Claims
if err := tok.Claims(matching[0].Key, &claims); err != nil {
t.Errorf("go-jose verify[%d] (%s) with parsed JWKS: %v", i, ks.AlgName, err)
}
}
}
func TestJWKSInterop_GoJoseToOur(t *testing.T) {
// Build a go-jose key set.
var joseJWKS jose.JSONWebKeySet
var sets []testkeys.KeySet
for _, ag := range testkeys.AllAlgorithms() {
ks := ag.Generate("jose-jwks-" + ag.Name)
sets = append(sets, ks)
joseJWKS.Keys = append(joseJWKS.Keys, jose.JSONWebKey{
Key: ks.RawPub,
KeyID: ks.KID,
Algorithm: ks.AlgName,
Use: "sig",
})
}
// Serialize go-jose JWKS.
jwksData, err := json.Marshal(joseJWKS)
if err != nil {
t.Fatal(err)
}
// Parse with our library.
var ourJWKS jwt.WellKnownJWKs
if err := json.Unmarshal(jwksData, &ourJWKS); err != nil {
t.Fatalf("our unmarshal of go-jose JWKS: %v", err)
}
if len(ourJWKS.Keys) != 5 {
t.Fatalf("expected 5 keys, got %d", len(ourJWKS.Keys))
}
verifier, _ := jwt.NewVerifier(ourJWKS.Keys)
// Sign tokens with go-jose, verify with our library.
for _, ks := range sets {
sigKey := jose.SigningKey{
Algorithm: joseAlg(ks.AlgName),
Key: jose.JSONWebKey{
Key: ks.RawPriv,
KeyID: ks.KID,
},
}
joseSigner, err := jose.NewSigner(sigKey, nil)
if err != nil {
t.Fatalf("go-jose signer %s: %v", ks.AlgName, err)
}
claims := josejwt.Claims{
Subject: "jose-to-our",
Expiry: josejwt.NewNumericDate(time.Now().Add(time.Hour)),
}
tokenStr, err := josejwt.Signed(joseSigner).Claims(claims).Serialize()
if err != nil {
t.Fatalf("go-jose sign %s: %v", ks.AlgName, err)
}
if _, err := verifier.VerifyJWT(tokenStr); err != nil {
t.Errorf("our verify %s from go-jose JWKS: %v", ks.AlgName, err)
}
}
}
// --- Audience interop ---
func TestAudienceStringInterop_GoJose(t *testing.T) {
ks := testkeys.GenerateEdDSA("aud-test")
// Our library: single aud marshals as plain string "single-aud".
signer, err := jwt.NewSigner([]*jwt.PrivateKey{ks.PrivKey})
if err != nil {
t.Fatal(err)
}
claims := testkeys.ListishClaims("aud-str", jwt.Listish{"single-aud"})
tokenStr, err := signer.SignToString(claims)
if err != nil {
t.Fatal(err)
}
tok, err := josejwt.ParseSigned(tokenStr, []jose.SignatureAlgorithm{jose.EdDSA})
if err != nil {
t.Fatalf("go-jose parse: %v", err)
}
var joseClaims josejwt.Claims
if err := tok.Claims(ks.RawPub, &joseClaims); err != nil {
t.Fatalf("go-jose Claims: %v", err)
}
if len(joseClaims.Audience) != 1 || joseClaims.Audience[0] != "single-aud" {
t.Errorf("aud: got %v, want [single-aud]", joseClaims.Audience)
}
// Reverse: go-jose signs with single aud, our library parses.
sigKey := jose.SigningKey{
Algorithm: jose.EdDSA,
Key: jose.JSONWebKey{Key: ks.RawPriv, KeyID: ks.KID},
}
joseSigner, _ := jose.NewSigner(sigKey, nil)
jClaims := josejwt.Claims{
Subject: "aud-str-rev",
Audience: josejwt.Listish{"single-aud"},
Expiry: josejwt.NewNumericDate(time.Now().Add(time.Hour)),
}
joseToken, _ := josejwt.Signed(joseSigner).Claims(jClaims).Serialize()
verifier, _ := jwt.NewVerifier([]jwt.PublicKey{ks.PubKey})
verifiedJWS, err := verifier.VerifyJWT(joseToken)
if err != nil {
t.Fatal(err)
}
var decoded jwt.TokenClaims
verifiedJWS.UnmarshalClaims(&decoded)
if len(decoded.Aud) == 0 || decoded.Aud[0] != "single-aud" {
t.Errorf("reverse aud: got %v, want [single-aud]", decoded.Aud)
}
}
func TestAudienceArrayInterop_GoJose(t *testing.T) {
ks := testkeys.GenerateEdDSA("aud-arr")
signer, _ := jwt.NewSigner([]*jwt.PrivateKey{ks.PrivKey})
claims := testkeys.ListishClaims("aud-arr", jwt.Listish{"aud1", "aud2"})
tokenStr, _ := signer.SignToString(claims)
tok, err := josejwt.ParseSigned(tokenStr, []jose.SignatureAlgorithm{jose.EdDSA})
if err != nil {
t.Fatalf("go-jose parse: %v", err)
}
var joseClaims josejwt.Claims
if err := tok.Claims(ks.RawPub, &joseClaims); err != nil {
t.Fatal(err)
}
if len(joseClaims.Audience) != 2 || joseClaims.Audience[0] != "aud1" || joseClaims.Audience[1] != "aud2" {
t.Errorf("aud: got %v, want [aud1 aud2]", joseClaims.Audience)
}
}
// --- Custom claims interop ---
func TestCustomClaimsInterop_GoJose(t *testing.T) {
ks := testkeys.GenerateEdDSA("custom")
signer, _ := jwt.NewSigner([]*jwt.PrivateKey{ks.PrivKey})
claims := &testkeys.CustomClaims{
TokenClaims: *testkeys.TestClaims("custom-user"),
Email: "user@example.com",
Roles: []string{"admin", "editor"},
Metadata: map[string]string{"team": "platform"},
}
tokenStr, err := signer.SignToString(claims)
if err != nil {
t.Fatal(err)
}
tok, err := josejwt.ParseSigned(tokenStr, []jose.SignatureAlgorithm{jose.EdDSA})
if err != nil {
t.Fatalf("go-jose parse: %v", err)
}
// go-jose extracts into an arbitrary struct.
var extracted struct {
josejwt.Claims
Email string `json:"email"`
Roles []string `json:"roles"`
Metadata map[string]string `json:"metadata"`
}
if err := tok.Claims(ks.RawPub, &extracted); err != nil {
t.Fatalf("go-jose Claims: %v", err)
}
if extracted.Email != "user@example.com" {
t.Errorf("email: got %q, want %q", extracted.Email, "user@example.com")
}
if len(extracted.Roles) != 2 || extracted.Roles[0] != "admin" {
t.Errorf("roles: got %v, want [admin editor]", extracted.Roles)
}
if extracted.Metadata["team"] != "platform" {
t.Errorf("metadata.team: got %v, want %q", extracted.Metadata["team"], "platform")
}
}
// --- NumericDate precision ---
func TestNumericDatePrecision_GoJose(t *testing.T) {
ks := testkeys.GenerateEdDSA("nd")
// Use fixed future timestamps to test precision without triggering
// expiration validation. 2000000000 = 2033-05-18.
var wantExp int64 = 2000000000
var wantIat int64 = 1999999000
claims := &jwt.TokenClaims{
Iss: "https://example.com",
Sub: "numdate",
Exp: wantExp,
IAt: wantIat,
}
signer, _ := jwt.NewSigner([]*jwt.PrivateKey{ks.PrivKey})
tokenStr, _ := signer.SignToString(claims)
tok, err := josejwt.ParseSigned(tokenStr, []jose.SignatureAlgorithm{jose.EdDSA})
if err != nil {
t.Fatal(err)
}
var joseClaims josejwt.Claims
if err := tok.Claims(ks.RawPub, &joseClaims); err != nil {
t.Fatal(err)
}
if joseClaims.Expiry.Time().Unix() != wantExp {
t.Errorf("exp: got %d, want %d", joseClaims.Expiry.Time().Unix(), wantExp)
}
if joseClaims.IssuedAt.Time().Unix() != wantIat {
t.Errorf("iat: got %d, want %d", joseClaims.IssuedAt.Time().Unix(), wantIat)
}
// Reverse: go-jose signs with specific times, our library reads.
var wantExp2 int64 = 2100000000
var wantIat2 int64 = 2099999000
sigKey := jose.SigningKey{
Algorithm: jose.EdDSA,
Key: jose.JSONWebKey{Key: ks.RawPriv, KeyID: ks.KID},
}
joseSigner, _ := jose.NewSigner(sigKey, nil)
jClaims := josejwt.Claims{
Subject: "numdate-rev",
Expiry: josejwt.NewNumericDate(time.Unix(wantExp2, 0)),
IssuedAt: josejwt.NewNumericDate(time.Unix(wantIat2, 0)),
}
joseToken, _ := josejwt.Signed(joseSigner).Claims(jClaims).Serialize()
verifier, _ := jwt.NewVerifier([]jwt.PublicKey{ks.PubKey})
verifiedJWS, _ := verifier.VerifyJWT(joseToken)
var decoded jwt.TokenClaims
verifiedJWS.UnmarshalClaims(&decoded)
if decoded.Exp != wantExp2 {
t.Errorf("rev exp: got %d, want %d", decoded.Exp, wantExp2)
}
if decoded.IAt != wantIat2 {
t.Errorf("rev iat: got %d, want %d", decoded.IAt, wantIat2)
}
}
// --- Stress tests ---
func TestStress_GoJose(t *testing.T) {
for _, ag := range testkeys.AllAlgorithms() {
ag := ag
t.Run(ag.Name, func(t *testing.T) {
t.Parallel()
n := 1000
if ag.Name == "RS256" {
n = 10
if *longTests {
n = 100
}
}
for i := range n {
ks := ag.Generate(fmt.Sprintf("s%d", i))
sub := fmt.Sprintf("stress-%d", i)
// Our sign, go-jose verify.
signer, err := jwt.NewSigner([]*jwt.PrivateKey{ks.PrivKey})
if err != nil {
t.Fatalf("iter %d: NewSigner: %v", i, err)
}
tokenStr, err := signer.SignToString(testkeys.TestClaims(sub))
if err != nil {
t.Fatalf("iter %d: SignToString: %v", i, err)
}
tok, err := josejwt.ParseSigned(tokenStr, []jose.SignatureAlgorithm{joseAlg(ks.AlgName)})
if err != nil {
t.Fatalf("iter %d: go-jose parse: %v", i, err)
}
var claims josejwt.Claims
if err := tok.Claims(ks.RawPub, &claims); err != nil {
t.Fatalf("iter %d: go-jose verify: %v", i, err)
}
// go-jose sign, our verify.
sigKey := jose.SigningKey{
Algorithm: joseAlg(ks.AlgName),
Key: jose.JSONWebKey{Key: ks.RawPriv, KeyID: ks.KID},
}
joseSigner, err := jose.NewSigner(sigKey, nil)
if err != nil {
t.Fatalf("iter %d: go-jose NewSigner: %v", i, err)
}
jClaims := josejwt.Claims{
Subject: sub,
Expiry: josejwt.NewNumericDate(time.Now().Add(time.Hour)),
}
joseToken, err := josejwt.Signed(joseSigner).Claims(jClaims).Serialize()
if err != nil {
t.Fatalf("iter %d: go-jose sign: %v", i, err)
}
verifier, _ := jwt.NewVerifier([]jwt.PublicKey{ks.PubKey})
if _, err := verifier.VerifyJWT(joseToken); err != nil {
t.Fatalf("iter %d: our verify: %v", i, err)
}
}
})
}
}