Files
upstream-centrifugo/internal/jwtverify/token_verifier_jwt_test.go
upstream-mirror b352a9a30d
Some checks failed
test / Lint (push) Failing after 3s
test / Test with Go 1.26.8 (push) Failing after 2s
snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:17:21 +05:00

1107 lines
36 KiB
Go

package jwtverify
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
"github.com/centrifugal/centrifugo/v6/internal/config"
"github.com/centrifugal/centrifuge"
"github.com/cristalhq/jwt/v5"
"github.com/stretchr/testify/require"
)
// Use https://jwt.io to look at token contents.
// noinspection ALL
const (
jwtValid = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyNjk0IiwiaW5mbyI6eyJmaXJzdF9uYW1lIjoiQWxleGFuZGVyIiwibGFzdF9uYW1lIjoiRW1lbGluIn19.m-TaS80RxkAiP9jH_s_h2NrKS_TDuPxJ8-z6gI7UewI"
jwtExpired = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyNjk0IiwiaW5mbyI6eyJmaXJzdF9uYW1lIjoiQWxleGFuZGVyIiwibGFzdF9uYW1lIjoiRW1lbGluIn0sImV4cCI6MTU4ODM1MTcwNH0.LTc0p5YlrwJcxXPETrjhm9qyYUBKCR5fSROmfCE4TD8"
jwtNotBefore = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyNjk0IiwiaW5mbyI6eyJmaXJzdF9uYW1lIjoiQWxleGFuZGVyIiwibGFzdF9uYW1lIjoiRW1lbGluIn0sImV4cCI6MzE3NjgyMDU3MCwibmJmIjozMTc2ODIwNTYwfQ.gfsQeznFw6g44OEnCTSBW7AkmLy92GBfXL_Bdvzs7vc"
jwtInvalidSignature = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyNjk0IiwiaW5mbyI6eyJmaXJzdF9uYW1lIjoiQWxleGFuZGVyIiwibGFzdF9uYW1lIjoiRW1lbGluIn0sImV4cCI6MTU4ODQxOTY5MywibmJmIjoxNTg4NDE4NjkzfQ.05Xj9adbLukdhSJFyiVUEgbxCHTajXuotmalFgYviCo"
jwtArrayAud = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyNjk0IiwiaW5mbyI6eyJmaXJzdF9uYW1lIjoiQWxleGFuZGVyIiwibGFzdF9uYW1lIjoiRW1lbGluIn0sImF1ZCI6WyJmb28iLCJiYXIiXX0.iY4pCPEQwstfNmPkLr7r7DrLZDo42q3E9jMc-TefI6g"
jwtStringAud = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyNjk0IiwiaW5mbyI6eyJmaXJzdF9uYW1lIjoiQWxleGFuZGVyIiwibGFzdF9uYW1lIjoiRW1lbGluIn0sImF1ZCI6ImZvbyJ9.jym6CG5haHME3ZQbb9jlnV1E0hSwwEjZycBZSygRzO0"
jwtValidCustomUserClaim = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGVzdCJ9.Mdh4PGRnqKD-8_cKCJOYKfi9KNLJz2PCKl3qEi0n0-w"
subJWTValidCustomUserClaim = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGVzdCIsImNoYW5uZWwiOiJjaGFubmVsIn0.vMA6Ee2eq3d8ApAhbXmVv5LmArbrjFZgU2FUbK93EnQ"
emptyObjectClaimsJWT = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.e30.DMCAvRgzrcf5w0Z879BsqzcrnDFKBY_GN6c3qKOUFtQ"
//
// Generated with: https://github.com/lestrrat-go/jwx/tree/main/cmd/jwx
//
// ```
// jwx jwk generate --type RSA --keysize 2048 --template '{"kid":"testrsa"}' > rsa.jwk
// jwx jwk generate --type EC --curve P-384 --template '{"kid":"testec"}' > ec.jwk
// jwx jwk generate --type OKP --curve Ed25519 --template '{"kid":"tested"}' > okp.jwk
// jwx jwk generate --type EC --curve P-384 --template '{"kid":"fakeid"}' > fakeid.jwk
// printf '{"sub":"2694","info":{"first_name":"Alexander","last_name":"Emelin"}}' > payload.txt
// ```
// JWK Set with RSA, EC and OKP keys:
// ```
// jwx jwk fmt --public-key rsa.jwk
// jwx jwk fmt --public-key ec.jwk
// jwx jwk fmt --public-key okp.jwk
// ```
jwksSet = `{
"keys": [
{
"e": "AQAB",
"kid": "testrsa",
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"n": "4Wp1fHDDOGN7rH357ofNfK26LDOA36ZtQ0H2x0uo12VsAxbGOfl67gES28ClWon9dSwGLR-urfAmX7DcCgffLMTgwCwvsPYCKsVIWMPvlGEPyAG90d55GVqJGpAYirfIVyjKkzJKIjqdmPx12XnjnrhWdTLl09Ja4E6SF5m1Ff4mkfavigrnuh_SaB1QKkMKj--ie0rH3VV9MAiQTnYkVuNPEEkz9h2SCyOMUmYLJMLIHIpWBZ4fI-XlCmFx_kGUgiU85m9lSoKFSl7zmvYvy-uxCteO_28COu-wLnhcN4uumnQKN13ESPXLtR7_fkP-Z-xlXoKMdZQfuWY6zc6AsQ"
},
{
"crv": "P-384",
"kid": "testec",
"kty": "EC",
"use": "sig",
"alg": "ES384",
"x": "W0A0VvKCnxs0trdgchvdkrEVfdjDYOeTdu_f0l3GE94LXBvVF_2O1Ng7vKZPE3cu",
"y": "thDvOCDapgLR4krw5KKzp9HrkzTVgVwmwP37aTSc20EXw3R2fZ7tSh1ws3V7NV5n"
},
{
"crv": "Ed25519",
"d": "hgIMcVff-mdWy5xYFBqrkleEGVSiQu81GQwNxGxhj9k",
"kid": "tested",
"kty": "OKP",
"use": "sig",
"alg" : "EdDSA",
"x": "KBbdGhSAMLXMh6zLMfGi4_4-npVhnEVnGVYdjSrOreI"
}
]
}`
jwksSetNoAlg = `{
"keys": [
{
"e": "AQAB",
"kid": "testrsa",
"kty": "RSA",
"use": "sig",
"n": "4Wp1fHDDOGN7rH357ofNfK26LDOA36ZtQ0H2x0uo12VsAxbGOfl67gES28ClWon9dSwGLR-urfAmX7DcCgffLMTgwCwvsPYCKsVIWMPvlGEPyAG90d55GVqJGpAYirfIVyjKkzJKIjqdmPx12XnjnrhWdTLl09Ja4E6SF5m1Ff4mkfavigrnuh_SaB1QKkMKj--ie0rH3VV9MAiQTnYkVuNPEEkz9h2SCyOMUmYLJMLIHIpWBZ4fI-XlCmFx_kGUgiU85m9lSoKFSl7zmvYvy-uxCteO_28COu-wLnhcN4uumnQKN13ESPXLtR7_fkP-Z-xlXoKMdZQfuWY6zc6AsQ"
},
{
"crv": "P-384",
"kid": "testec",
"kty": "EC",
"use": "sig",
"x": "W0A0VvKCnxs0trdgchvdkrEVfdjDYOeTdu_f0l3GE94LXBvVF_2O1Ng7vKZPE3cu",
"y": "thDvOCDapgLR4krw5KKzp9HrkzTVgVwmwP37aTSc20EXw3R2fZ7tSh1ws3V7NV5n"
},
{
"crv": "Ed25519",
"d": "hgIMcVff-mdWy5xYFBqrkleEGVSiQu81GQwNxGxhj9k",
"kid": "tested",
"kty": "OKP",
"use": "sig",
"x": "KBbdGhSAMLXMh6zLMfGi4_4-npVhnEVnGVYdjSrOreI"
}
]
}`
// JWT using `testrsa` key ID
// ```
// jwx jws sign --key rsa.jwk --alg RS256 payload.txt
// ```
jwtRSAWithKID = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Ryc2EifQ.eyJzdWIiOiIyNjk0IiwiaW5mbyI6eyJmaXJzdF9uYW1lIjoiQWxleGFuZGVyIiwibGFzdF9uYW1lIjoiRW1lbGluIn19.Kq6upfpD3WdTpupDH0PL9lgjAr5OUO0RMB_w6F6M67qWH36noeXXZ1G-_BlOow-a9Lwrdtiy5-WVXC3-wbADaNsOoMSLWWGH-9lxqQ5Df9cJoUu4Ocvm9S0vSB9G2e0yRqwKXL-UC_M1BZonHIwBPs5lCmU_1LynkGJ0uZSvVI8Ke5VQwLFeBsxjNpCbkVc0mAlES4uoD49nh0rTqXmyNoPcriFtxEyUIvdVHe8bKkEBC4yEhg7IevApD8BjY4M6btcOlZYmXxdVSHnNsShQS1WhVhTyqxYDj4ad7rNOZYz4_PE0hEKqSyNxNuJpYimc7aBImCDTgcewJh43Vygcdw"
// JWT using `testec` key ID
// ```
// jwx jws sign --key ec.jwk --alg ES384 payload.txt
// ```
jwtECWithKID = "eyJhbGciOiJFUzM4NCIsImtpZCI6InRlc3RlYyJ9.eyJzdWIiOiIyNjk0IiwiaW5mbyI6eyJmaXJzdF9uYW1lIjoiQWxleGFuZGVyIiwibGFzdF9uYW1lIjoiRW1lbGluIn19.MFqUDsu8JjS99oROB_93JEwuGlWOnrmMK455UIjWDlSS62uwED7hE04ZcWyQClxnH88Z-oiU_qGnpTyi7zojMN9r3zBxXqdq-wyTqUH6P9NA5PIlXhh6pugLpGKwyfQG"
// JWT using `tested` key ID
// ```
// jwx jws sign --key okp.jwk --alg EdDSA payload.txt
// ```
jwtOKPWithKID = "eyJhbGciOiJFZERTQSIsImtpZCI6InRlc3RlZCJ9.eyJzdWIiOiIyNjk0IiwiaW5mbyI6eyJmaXJzdF9uYW1lIjoiQWxleGFuZGVyIiwibGFzdF9uYW1lIjoiRW1lbGluIn19.upmjjFmTOk9A-gtkA8fPqfj0YAsETvXYotLNFrf86AyHBpQNRMw2ScKbRx6BHnG0klCbJ0C2dfJt501zx5WhCA"
// JWT using `fakekid` key ID
// ```
// jwx jws sign --key ec.jwk --alg ES384 payload.txt
// ```
jwtECWithKIDMiss = "eyJhbGciOiJFUzM4NCIsImtpZCI6ImZha2VpZCJ9.eyJzdWIiOiIyNjk0IiwiaW5mbyI6eyJmaXJzdF9uYW1lIjoiQWxleGFuZGVyIiwibGFzdF9uYW1lIjoiRW1lbGluIn19.bmsj2WNZK1wnbzXs8pKRjvljHIgQK9g4N7_M2-tD5PAXpHqiP7ttqsyflIOOAbUVdIix4JOL_Dtv4GQsEof87AOsi--fPFaXkVXFQzx5MggshXn02FbwHQ0Mj6LOw1qC"
// JWT using `testec` key ID but wrong alg
// ```
// jwx jws sign --key ec.jwk --alg ES256 payload.txt
// ```
jwtECWithKIDWrongAlg = "eyJhbGciOiJFUzI1NiIsImtpZCI6InRlc3RlYyJ9.eyJzdWIiOiIyNjk0IiwiaW5mbyI6eyJmaXJzdF9uYW1lIjoiQWxleGFuZGVyIiwibGFzdF9uYW1lIjoiRW1lbGluIn19.P7vuso8O-x7yzobSlfbyA7JiscnViLl303-5kAO7_6CZ3jWAOaZqllOrZfCTJgCoZlX5lNGgW6naJ3zKuOkO434osi6VeN1oaUl0-IHysmlP7Za5QNXzfUoKN6aCuJx7"
)
const nullByte = 0x0
func encodeToString(src []byte) string {
return base64.RawURLEncoding.EncodeToString(src)
}
func encodeUint64ToString(v uint64) string {
data := make([]byte, 8)
binary.BigEndian.PutUint64(data, v)
i := 0
for ; i < len(data); i++ {
if data[i] != nullByte {
break
}
}
return encodeToString(data[i:])
}
func generateTestRSAKeys(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey) {
reader := rand.Reader
bitSize := 2048
key, err := rsa.GenerateKey(reader, bitSize)
require.NoError(t, err)
return key, &key.PublicKey
}
func generateTestECDSAKeys(t *testing.T) (*ecdsa.PrivateKey, *ecdsa.PublicKey) {
reader := rand.Reader
key, err := ecdsa.GenerateKey(elliptic.P256(), reader)
require.NoError(t, err)
return key, &key.PublicKey
}
func getRSATokenBuilder(rsaPrivateKey *rsa.PrivateKey, opts ...jwt.BuilderOption) *jwt.Builder {
var signer jwt.Signer
if rsaPrivateKey != nil {
signer, _ = jwt.NewSignerRS(jwt.RS256, rsaPrivateKey)
} else {
// For HS we do everything in tests with key `secret`.
key := []byte(`secret`)
signer, _ = jwt.NewSignerHS(jwt.HS256, key)
}
return jwt.NewBuilder(signer, opts...)
}
func getECDSATokenBuilder(ecdsaPrivateKey *ecdsa.PrivateKey, opts ...jwt.BuilderOption) *jwt.Builder {
var signer jwt.Signer
if ecdsaPrivateKey != nil {
signer, _ = jwt.NewSignerES(jwt.ES256, ecdsaPrivateKey)
} else {
// For HS we do everything in tests with key `secret`.
key := []byte(`secret`)
signer, _ = jwt.NewSignerHS(jwt.HS256, key)
}
return jwt.NewBuilder(signer, opts...)
}
func getRSAConnToken(user string, exp int64, rsaPrivateKey *rsa.PrivateKey, opts ...jwt.BuilderOption) string {
builder := getRSATokenBuilder(rsaPrivateKey, opts...)
claims := &ConnectTokenClaims{
Base64Info: "e30=",
RegisteredClaims: jwt.RegisteredClaims{
Subject: user,
Audience: []string{"test"},
Issuer: "test",
},
}
if exp > 0 {
claims.ExpiresAt = jwt.NewNumericDate(time.Unix(exp, 0))
}
token, err := builder.Build(claims)
if err != nil {
panic(err)
}
return token.String()
}
func getECDSAConnToken(user string, exp int64, ecdsaPrivateKey *ecdsa.PrivateKey, opts ...jwt.BuilderOption) string {
builder := getECDSATokenBuilder(ecdsaPrivateKey, opts...)
claims := &ConnectTokenClaims{
Base64Info: "e30=",
RegisteredClaims: jwt.RegisteredClaims{
Subject: user,
},
}
if exp > 0 {
claims.ExpiresAt = jwt.NewNumericDate(time.Unix(exp, 0))
}
token, err := builder.Build(claims)
if err != nil {
panic(err)
}
return token.String()
}
func getRSASubscribeToken(channel string, client string, exp int64, rsaPrivateKey *rsa.PrivateKey) string {
builder := getRSATokenBuilder(rsaPrivateKey)
claims := &SubscribeTokenClaims{
SubscribeOptions: SubscribeOptions{
Base64Info: "e30=",
},
Channel: channel,
Client: client,
RegisteredClaims: jwt.RegisteredClaims{},
}
if exp > 0 {
claims.ExpiresAt = jwt.NewNumericDate(time.Unix(exp, 0))
}
token, err := builder.Build(claims)
if err != nil {
panic(err)
}
return token.String()
}
func getECDSASubscribeToken(channel string, client string, exp int64, ecdsaPrivateKey *ecdsa.PrivateKey) string {
builder := getECDSATokenBuilder(ecdsaPrivateKey)
claims := &SubscribeTokenClaims{
SubscribeOptions: SubscribeOptions{
Base64Info: "e30=",
},
Channel: channel,
Client: client,
RegisteredClaims: jwt.RegisteredClaims{},
}
if exp > 0 {
claims.ExpiresAt = jwt.NewNumericDate(time.Unix(exp, 0))
}
token, err := builder.Build(claims)
if err != nil {
panic(err)
}
return token.String()
}
func getJWKServer(pubKey *rsa.PublicKey, kty, use, kid string) *httptest.Server {
return httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := map[string]any{
"keys": []map[string]string{
{
"alg": "RS256",
"kty": kty,
"use": use,
"kid": kid,
"n": encodeToString(pubKey.N.Bytes()),
"e": encodeUint64ToString(uint64(pubKey.E)),
},
}}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
}
func Test_tokenVerifierJWT_Signer(t *testing.T) {
_, rsaPubKey := generateTestRSAKeys(t)
_, ecdsaPubKey := generateTestECDSAKeys(t)
signer, err := newAlgorithms("secret", rsaPubKey, ecdsaPubKey)
require.NoError(t, err)
require.NotNil(t, signer)
}
func Test_tokenVerifierJWT_Valid(t *testing.T) {
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifier, err := NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret"}, cfgContainer)
require.NoError(t, err)
ct, err := verifier.VerifyConnectToken(jwtValid, false)
require.NoError(t, err)
require.Equal(t, "2694", ct.UserID)
require.NotNil(t, ct.Info)
require.Equal(t, `{"first_name":"Alexander","last_name":"Emelin"}`, string(ct.Info))
}
func Test_tokenVerifierJWT_Valid_CustomClaim(t *testing.T) {
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
// Test that by default `user_id` claim is ignored.
verifier, err := NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret"}, cfgContainer)
require.NoError(t, err)
ct, err := verifier.VerifyConnectToken(jwtValidCustomUserClaim, false)
require.NoError(t, err)
require.Equal(t, "", ct.UserID)
// Now test that custom `user_id` claim works for connection token.
verifier, err = NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", UserIDClaim: "user_id"}, cfgContainer)
require.NoError(t, err)
ct, err = verifier.VerifyConnectToken(jwtValidCustomUserClaim, false)
require.NoError(t, err)
require.Equal(t, "test", ct.UserID)
// And the same for subscription token.
verifier, err = NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret"}, cfgContainer)
require.NoError(t, err)
st, err := verifier.VerifySubscribeToken(subJWTValidCustomUserClaim, false)
require.NoError(t, err)
require.Equal(t, "", st.UserID)
require.Equal(t, "channel", st.Channel)
verifier, err = NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", UserIDClaim: "user_id"}, cfgContainer)
require.NoError(t, err)
st, err = verifier.VerifySubscribeToken(subJWTValidCustomUserClaim, false)
require.NoError(t, err)
require.Equal(t, "test", st.UserID)
// Also make sure custom claim returns empty user ID from empty object claims token.
verifier, err = NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", UserIDClaim: "user_id"}, cfgContainer)
require.NoError(t, err)
ct, err = verifier.VerifyConnectToken(emptyObjectClaimsJWT, false)
require.NoError(t, err)
require.Equal(t, "", ct.UserID)
}
func Test_tokenVerifierJWT_Audience(t *testing.T) {
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifier, err := NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", Audience: "test2"}, cfgContainer)
require.NoError(t, err)
// Token without aud.
_, err = verifier.VerifyConnectToken(jwtValid, false)
require.ErrorIs(t, err, ErrInvalidToken)
// Generate token with aud.
token := getRSAConnToken("user", time.Now().Add(time.Hour).Unix(), nil)
// Verifier with audience which does not match aud in token.
verifier, err = NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", Audience: "test2"}, cfgContainer)
require.NoError(t, err)
_, err = verifier.VerifyConnectToken(token, false)
require.ErrorIs(t, err, ErrInvalidToken)
// Verifier with token audience.
verifier, err = NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", Audience: "test"}, cfgContainer)
require.NoError(t, err)
_, err = verifier.VerifyConnectToken(token, false)
require.NoError(t, err)
// Verifier with token audience - valid.
verifier, err = NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", AudienceRegex: "test"}, cfgContainer)
require.NoError(t, err)
_, err = verifier.VerifyConnectToken(token, false)
require.NoError(t, err)
// Verifier with token audience - invalid.
verifier, err = NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", AudienceRegex: "test2"}, cfgContainer)
require.NoError(t, err)
_, err = verifier.VerifyConnectToken(token, false)
require.Error(t, err)
}
func Test_tokenVerifierJWT_Issuer(t *testing.T) {
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifier, err := NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", Issuer: "test2"}, cfgContainer)
require.NoError(t, err)
// Token without iss.
_, err = verifier.VerifyConnectToken(jwtValid, false)
require.ErrorIs(t, err, ErrInvalidToken)
// Generate token with iss.
token := getRSAConnToken("user", time.Now().Add(time.Hour).Unix(), nil)
// Verifier with issuer which does not match token iss.
verifier, err = NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", Issuer: "test2"}, cfgContainer)
require.NoError(t, err)
_, err = verifier.VerifyConnectToken(token, false)
require.ErrorIs(t, err, ErrInvalidToken)
// Verifier with token issuer.
verifier, err = NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", Issuer: "test"}, cfgContainer)
require.NoError(t, err)
_, err = verifier.VerifyConnectToken(token, false)
require.NoError(t, err)
// Verifier with token issuer regex - valid.
verifier, err = NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", IssuerRegex: "test"}, cfgContainer)
require.NoError(t, err)
_, err = verifier.VerifyConnectToken(token, false)
require.NoError(t, err)
// Verifier with token issuer regex - invalid.
verifier, err = NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", IssuerRegex: "test2"}, cfgContainer)
require.NoError(t, err)
_, err = verifier.VerifyConnectToken(token, false)
require.Error(t, err)
}
func Test_tokenVerifierJWT_Expired(t *testing.T) {
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifier, err := NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret"}, cfgContainer)
require.NoError(t, err)
_, err = verifier.VerifyConnectToken(jwtExpired, false)
require.Error(t, err)
require.Equal(t, ErrTokenExpired, err)
}
func Test_tokenVerifierJWT_DisabledAlgorithm(t *testing.T) {
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifier, err := NewTokenVerifierJWT(VerifierConfig{}, cfgContainer)
require.NoError(t, err)
_, err = verifier.VerifyConnectToken(jwtExpired, false)
require.Error(t, err)
require.True(t, errors.Is(err, ErrInvalidToken), err.Error())
}
func Test_tokenVerifierJWT_InvalidSignature(t *testing.T) {
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifier, err := NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret"}, cfgContainer)
require.NoError(t, err)
_, err = verifier.VerifyConnectToken(jwtInvalidSignature, false)
require.Error(t, err)
// Test that skipVerify results into accepted token.
ct, err := verifier.VerifyConnectToken(jwtValid+"xxx", true)
require.NoError(t, err)
require.Equal(t, "2694", ct.UserID)
}
func Test_tokenVerifierJWT_InvalidSignature_SkipVerify(t *testing.T) {
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifier, err := NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret"}, cfgContainer)
require.NoError(t, err)
ct, err := verifier.VerifyConnectToken(jwtValid+"xxx", true)
require.NoError(t, err)
require.Equal(t, "2694", ct.UserID)
}
func Test_tokenVerifierJWT_WithNotBefore(t *testing.T) {
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifier, err := NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret"}, cfgContainer)
require.NoError(t, err)
_, err = verifier.VerifyConnectToken(jwtNotBefore, false)
require.Error(t, err)
// Test that skipVerify still results into unaccepted token if it's expired.
_, err = verifier.VerifyConnectToken(jwtNotBefore, true)
require.Error(t, err)
}
func Test_tokenVerifierJWT_StringAudience(t *testing.T) {
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifier, err := NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret"}, cfgContainer)
require.NoError(t, err)
ct, err := verifier.VerifyConnectToken(jwtStringAud, false)
require.NoError(t, err)
require.Equal(t, "2694", ct.UserID)
}
func Test_tokenVerifierJWT_ArrayAudience(t *testing.T) {
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifier, err := NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret"}, cfgContainer)
require.NoError(t, err)
ct, err := verifier.VerifyConnectToken(jwtArrayAud, false)
require.NoError(t, err)
require.Equal(t, "2694", ct.UserID)
}
func Test_tokenVerifierJWT_VerifyConnectToken(t *testing.T) {
type args struct {
token string
}
rsaPrivateKey, rsaPubKey := generateTestRSAKeys(t)
ecdsaPrivateKey, ecdsaPubKey := generateTestECDSAKeys(t)
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifierJWT, err := NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", RSAPublicKey: rsaPubKey, ECDSAPublicKey: ecdsaPubKey}, cfgContainer)
require.NoError(t, err)
_time := time.Now()
tests := []struct {
name string
verifier Verifier
args args
want ConnectToken
wantErr bool
expired bool
}{
{
name: "Valid JWT HS",
verifier: verifierJWT,
args: args{
token: getRSAConnToken("user1", _time.Add(24*time.Hour).Unix(), nil),
},
want: ConnectToken{
UserID: "user1",
ExpireAt: _time.Add(24 * time.Hour).Unix(),
Info: []byte("{}"),
Subs: map[string]centrifuge.SubscribeOptions{},
},
wantErr: false,
}, {
name: "Valid JWT RS",
verifier: verifierJWT,
args: args{
token: getRSAConnToken("user1", _time.Add(24*time.Hour).Unix(), rsaPrivateKey),
},
want: ConnectToken{
UserID: "user1",
ExpireAt: _time.Add(24 * time.Hour).Unix(),
Info: []byte("{}"),
Subs: map[string]centrifuge.SubscribeOptions{},
},
wantErr: false,
},
{
name: "Valid JWT ES",
verifier: verifierJWT,
args: args{
token: getECDSAConnToken("user1", _time.Add(24*time.Hour).Unix(), ecdsaPrivateKey),
},
want: ConnectToken{
UserID: "user1",
ExpireAt: _time.Add(24 * time.Hour).Unix(),
Info: []byte("{}"),
Subs: map[string]centrifuge.SubscribeOptions{},
},
wantErr: false,
},
{
name: "Invalid JWT",
verifier: verifierJWT,
args: args{
token: "Invalid jwt",
},
want: ConnectToken{},
wantErr: true,
expired: false,
}, {
name: "Expired JWT",
verifier: verifierJWT,
args: args{
token: getRSAConnToken("user1", _time.Add(-24*time.Hour).Unix(), nil),
},
want: ConnectToken{},
wantErr: true,
expired: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.verifier.VerifyConnectToken(tt.args.token, false)
if tt.wantErr && err == nil {
t.Errorf("VerifyConnectToken() should return error")
}
if !tt.wantErr && err != nil {
t.Errorf("VerifyConnectToken() should not return error")
}
if tt.expired && !errors.Is(err, ErrTokenExpired) {
t.Errorf("VerifyConnectToken() should return token expired error")
}
require.Equal(t, tt.want, got)
})
}
}
func Test_tokenVerifierJWT_VerifyConnectTokenWithJWK(t *testing.T) {
type token struct {
user string
exp int64
}
type jwk struct {
kty string
use string
kid string
}
now := time.Now()
testCases := []struct {
name string
token token
jwk jwk
want ConnectToken
wantErr bool
expired bool
}{
{
name: "OK",
token: token{user: "user1", exp: now.Add(24 * time.Hour).Unix()},
jwk: jwk{kty: "RSA", use: "sig", kid: "ok"},
want: ConnectToken{
UserID: "user1",
ExpireAt: now.Add(24 * time.Hour).Unix(),
Info: []byte("{}"),
Subs: map[string]centrifuge.SubscribeOptions{},
},
wantErr: false,
},
{
name: "ExpiredToken",
token: token{user: "user2", exp: now.Add(-24 * time.Hour).Unix()},
jwk: jwk{kty: "RSA", use: "sig", kid: "ok"},
want: ConnectToken{},
wantErr: false,
expired: true,
},
{
name: "InvalidKeyUsage",
token: token{user: "user3", exp: now.Add(24 * time.Hour).Unix()},
jwk: jwk{kty: "RSA", use: "enc", kid: "invalidkeyusage"},
want: ConnectToken{},
wantErr: true,
},
{
name: "InvalidKeyType",
token: token{user: "user4", exp: now.Add(24 * time.Hour).Unix()},
jwk: jwk{kty: "HS", use: "sig", kid: "invalidkeytype"},
want: ConnectToken{},
wantErr: true,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
r := require.New(t)
privKey, pubKey := generateTestRSAKeys(t)
ts := getJWKServer(pubKey, tt.jwk.kty, tt.jwk.use, tt.jwk.kid)
ts.Start()
defer ts.Close()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifier, err := NewTokenVerifierJWT(VerifierConfig{JWKSPublicEndpoint: ts.URL}, cfgContainer)
require.NoError(t, err)
token := getRSAConnToken(tt.token.user, tt.token.exp, privKey, jwt.WithKeyID(tt.jwk.kid))
got, err := verifier.VerifyConnectToken(token, false)
if tt.wantErr {
r.Error(err)
return
}
if tt.expired {
r.EqualError(err, ErrTokenExpired.Error())
return
}
r.NoError(err)
r.EqualValues(got, tt.want)
})
}
}
func Test_tokenVerifierJWT_VerifySubscribeToken(t *testing.T) {
type args struct {
token string
}
rsaPrivateKey, rsaPubKey := generateTestRSAKeys(t)
ecdsaPrivateKey, ecdsaPubKey := generateTestECDSAKeys(t)
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifierJWT, err := NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret", RSAPublicKey: rsaPubKey, ECDSAPublicKey: ecdsaPubKey}, cfgContainer)
require.NoError(t, err)
_time := time.Now()
tests := []struct {
name string
verifier Verifier
args args
want SubscribeToken
wantErr bool
expired bool
skipVerify bool
}{
{
name: "Empty JWT",
verifier: verifierJWT,
args: args{},
want: SubscribeToken{},
wantErr: true,
expired: false,
}, {
name: "Invalid JWT",
verifier: verifierJWT,
args: args{
token: "randomToken",
},
want: SubscribeToken{},
wantErr: true,
expired: false,
}, {
name: "Expired JWT",
verifier: verifierJWT,
args: args{
token: getRSASubscribeToken("channel1", "client1", _time.Add(-24*time.Hour).Unix(), nil),
},
want: SubscribeToken{},
wantErr: true,
expired: true,
}, {
name: "Valid JWT HS",
verifier: verifierJWT,
args: args{
token: getRSASubscribeToken("channel1", "client1", _time.Add(24*time.Hour).Unix(), nil),
},
want: SubscribeToken{
Client: "client1",
Channel: "channel1",
Options: centrifuge.SubscribeOptions{
ExpireAt: _time.Add(24 * time.Hour).Unix(),
ChannelInfo: []byte("{}"),
},
},
wantErr: false,
}, {
name: "Invalid JWT HS",
verifier: verifierJWT,
args: args{
token: getRSASubscribeToken("channel1", "client1", _time.Add(24*time.Hour).Unix(), nil) + "xxx",
},
want: SubscribeToken{},
wantErr: true,
skipVerify: false,
}, {
name: "Invalid JWT HS but verify skipped",
verifier: verifierJWT,
args: args{
token: getRSASubscribeToken("channel1", "client1", _time.Add(24*time.Hour).Unix(), nil) + "xxx",
},
want: SubscribeToken{
Client: "client1",
Channel: "channel1",
Options: centrifuge.SubscribeOptions{
ExpireAt: _time.Add(24 * time.Hour).Unix(),
ChannelInfo: []byte("{}"),
},
},
wantErr: false,
skipVerify: true,
}, {
name: "Valid JWT RS",
verifier: verifierJWT,
args: args{
token: getRSASubscribeToken("channel1", "client1", _time.Add(24*time.Hour).Unix(), rsaPrivateKey),
},
want: SubscribeToken{
Client: "client1",
Channel: "channel1",
Options: centrifuge.SubscribeOptions{
ExpireAt: _time.Add(24 * time.Hour).Unix(),
ChannelInfo: []byte("{}"),
},
},
wantErr: false,
},
{
name: "Valid JWT ES",
verifier: verifierJWT,
args: args{
token: getECDSASubscribeToken("channel1", "client1", _time.Add(24*time.Hour).Unix(), ecdsaPrivateKey),
},
want: SubscribeToken{
Client: "client1",
Channel: "channel1",
Options: centrifuge.SubscribeOptions{
ExpireAt: _time.Add(24 * time.Hour).Unix(),
ChannelInfo: []byte("{}"),
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.verifier.VerifySubscribeToken(tt.args.token, tt.skipVerify)
if tt.wantErr && err == nil {
t.Errorf("VerifySubscribeToken() should return error")
}
if !tt.wantErr && err != nil {
t.Errorf("VerifySubscribeToken() should not return error, but returned: %v", err)
}
if tt.expired && !errors.Is(err, ErrTokenExpired) {
t.Errorf("VerifySubscribeToken() should return token expired error")
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("VerifySubscribeToken() got = %v, want %v", got, tt.want)
}
})
}
}
func jwksHandler(json string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(json))
})
}
func TestJWKS(t *testing.T) {
// Create a test JWKS server.
ts := httptest.NewServer(jwksHandler(jwksSet))
defer ts.Close()
// Setup our token verifier, using the test JWKS endpoint
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifier, err := NewTokenVerifierJWT(VerifierConfig{JWKSPublicEndpoint: ts.URL}, cfgContainer)
require.NoError(t, err)
// Validate an RSA token
ct, err := verifier.VerifyConnectToken(jwtRSAWithKID, false)
require.NoError(t, err)
require.Equal(t, "2694", ct.UserID)
require.NotNil(t, ct.Info)
require.Equal(t, `{"first_name":"Alexander","last_name":"Emelin"}`, string(ct.Info))
// Validate an EC token
ct, err = verifier.VerifyConnectToken(jwtECWithKID, false)
require.NoError(t, err)
require.Equal(t, "2694", ct.UserID)
require.NotNil(t, ct.Info)
require.Equal(t, `{"first_name":"Alexander","last_name":"Emelin"}`, string(ct.Info))
// Validate OKP (based on EdDSA) token.
ct, err = verifier.VerifyConnectToken(jwtOKPWithKID, false)
require.NoError(t, err)
require.Equal(t, "2694", ct.UserID)
require.NotNil(t, ct.Info)
require.Equal(t, `{"first_name":"Alexander","last_name":"Emelin"}`, string(ct.Info))
// Make sure a token with an unknown KID fails to verify
_, err = verifier.VerifyConnectToken(jwtECWithKIDMiss, false)
require.ErrorContains(t, err, "invalid token: jwks: public key not found")
// Make sure a token with an unknown KID fails to verify
_, err = verifier.VerifyConnectToken(jwtECWithKIDWrongAlg, false)
require.ErrorContains(t, err, "invalid token: token is signed by another algorithm")
}
func TestJWKS_NoAlg(t *testing.T) {
// Create a test JWKS server.
ts := httptest.NewServer(jwksHandler(jwksSetNoAlg))
defer ts.Close()
// Setup our token verifier, using the test JWKS endpoint
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
verifier, err := NewTokenVerifierJWT(VerifierConfig{JWKSPublicEndpoint: ts.URL}, cfgContainer)
require.NoError(t, err)
// Validate an RSA token
ct, err := verifier.VerifyConnectToken(jwtRSAWithKID, false)
require.NoError(t, err)
require.Equal(t, "2694", ct.UserID)
require.NotNil(t, ct.Info)
require.Equal(t, `{"first_name":"Alexander","last_name":"Emelin"}`, string(ct.Info))
// Validate an EC token
ct, err = verifier.VerifyConnectToken(jwtECWithKID, false)
require.NoError(t, err)
require.Equal(t, "2694", ct.UserID)
require.NotNil(t, ct.Info)
require.Equal(t, `{"first_name":"Alexander","last_name":"Emelin"}`, string(ct.Info))
// Validate OKP (based on EdDSA) token.
ct, err = verifier.VerifyConnectToken(jwtOKPWithKID, false)
require.NoError(t, err)
require.Equal(t, "2694", ct.UserID)
require.NotNil(t, ct.Info)
require.Equal(t, `{"first_name":"Alexander","last_name":"Emelin"}`, string(ct.Info))
// Make sure a token with an unknown KID fails to verify
_, err = verifier.VerifyConnectToken(jwtECWithKIDMiss, false)
require.ErrorContains(t, err, "invalid token: jwks: public key not found")
// Make sure a token with an unknown KID fails to verify
_, err = verifier.VerifyConnectToken(jwtECWithKIDWrongAlg, false)
require.ErrorContains(t, err, "invalid token: unsupported JWT algorithm: ES256")
}
func getHMACConnToken(user string, exp int64, secret string) string {
signer, _ := jwt.NewSignerHS(jwt.HS256, []byte(secret))
builder := jwt.NewBuilder(signer)
claims := &ConnectTokenClaims{
Base64Info: "e30=",
RegisteredClaims: jwt.RegisteredClaims{
Subject: user,
},
}
if exp > 0 {
claims.ExpiresAt = jwt.NewNumericDate(time.Unix(exp, 0))
}
token, err := builder.Build(claims)
if err != nil {
panic(err)
}
return token.String()
}
func Test_tokenVerifierJWT_PreviousHMACKey(t *testing.T) {
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
newSecret := "new-secret"
oldSecret := "old-secret"
t.Run("token signed with previous key verifies", func(t *testing.T) {
verifier, err := NewTokenVerifierJWT(VerifierConfig{
HMACSecretKey: newSecret,
HMACPreviousSecretKey: oldSecret,
}, cfgContainer)
require.NoError(t, err)
token := getHMACConnToken("user1", time.Now().Add(time.Hour).Unix(), oldSecret)
ct, err := verifier.VerifyConnectToken(token, false)
require.NoError(t, err)
require.Equal(t, "user1", ct.UserID)
})
t.Run("token signed with current key still verifies", func(t *testing.T) {
verifier, err := NewTokenVerifierJWT(VerifierConfig{
HMACSecretKey: newSecret,
HMACPreviousSecretKey: oldSecret,
}, cfgContainer)
require.NoError(t, err)
token := getHMACConnToken("user1", time.Now().Add(time.Hour).Unix(), newSecret)
ct, err := verifier.VerifyConnectToken(token, false)
require.NoError(t, err)
require.Equal(t, "user1", ct.UserID)
})
t.Run("token signed with previous key fails after valid_until expired", func(t *testing.T) {
verifier, err := NewTokenVerifierJWT(VerifierConfig{
HMACSecretKey: newSecret,
HMACPreviousSecretKey: oldSecret,
HMACPreviousSecretKeyValidUntil: time.Now().Add(-time.Hour).Unix(), // Expired 1 hour ago.
}, cfgContainer)
require.NoError(t, err)
token := getHMACConnToken("user1", time.Now().Add(time.Hour).Unix(), oldSecret)
_, err = verifier.VerifyConnectToken(token, false)
require.Error(t, err)
require.ErrorIs(t, err, ErrInvalidToken)
})
t.Run("token signed with previous key works before valid_until", func(t *testing.T) {
verifier, err := NewTokenVerifierJWT(VerifierConfig{
HMACSecretKey: newSecret,
HMACPreviousSecretKey: oldSecret,
HMACPreviousSecretKeyValidUntil: time.Now().Add(time.Hour).Unix(), // Still valid.
}, cfgContainer)
require.NoError(t, err)
token := getHMACConnToken("user1", time.Now().Add(time.Hour).Unix(), oldSecret)
ct, err := verifier.VerifyConnectToken(token, false)
require.NoError(t, err)
require.Equal(t, "user1", ct.UserID)
})
t.Run("token signed with unknown key fails", func(t *testing.T) {
verifier, err := NewTokenVerifierJWT(VerifierConfig{
HMACSecretKey: newSecret,
HMACPreviousSecretKey: oldSecret,
}, cfgContainer)
require.NoError(t, err)
token := getHMACConnToken("user1", time.Now().Add(time.Hour).Unix(), "unknown-secret")
_, err = verifier.VerifyConnectToken(token, false)
require.Error(t, err)
require.ErrorIs(t, err, ErrInvalidToken)
})
t.Run("reload with new previous key works", func(t *testing.T) {
verifier, err := NewTokenVerifierJWT(VerifierConfig{
HMACSecretKey: newSecret,
HMACPreviousSecretKey: oldSecret,
}, cfgContainer)
require.NoError(t, err)
anotherOldSecret := "another-old-secret"
err = verifier.Reload(VerifierConfig{
HMACSecretKey: newSecret,
HMACPreviousSecretKey: anotherOldSecret,
})
require.NoError(t, err)
// Token signed with original old secret should no longer verify.
token := getHMACConnToken("user1", time.Now().Add(time.Hour).Unix(), oldSecret)
_, err = verifier.VerifyConnectToken(token, false)
require.Error(t, err)
// Token signed with new previous secret should verify.
token = getHMACConnToken("user1", time.Now().Add(time.Hour).Unix(), anotherOldSecret)
ct, err := verifier.VerifyConnectToken(token, false)
require.NoError(t, err)
require.Equal(t, "user1", ct.UserID)
})
}
func BenchmarkConnectTokenVerify_Valid(b *testing.B) {
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(b, err)
verifierJWT, err := NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret"}, cfgContainer)
require.NoError(b, err)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := verifierJWT.VerifyConnectToken(jwtValid, false)
if err != nil {
b.Fatal(err)
}
}
b.StopTimer()
b.ReportAllocs()
}
func BenchmarkConnectTokenVerify_Expired(b *testing.B) {
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(b, err)
verifier, err := NewTokenVerifierJWT(VerifierConfig{HMACSecretKey: "secret"}, cfgContainer)
require.NoError(b, err)
for i := 0; i < b.N; i++ {
_, err := verifier.VerifyConnectToken(jwtExpired, false)
if err != ErrTokenExpired {
panic(err)
}
}
}