1107 lines
36 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|