Files
rtsp-simple-server/internal/auth/manager_test.go
Alessandro Ros 68b4c20627 fix reading JWT when it is passed through the password field (#5009)
Usernames and passwords must be requested explicitly to clients, but
they were not requested when JWT is meant to be passed as password.
This fixes the issue.
2025-09-22 10:00:33 +02:00

587 lines
14 KiB
Go

package auth
import (
"context"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"net"
"net/http"
"testing"
"time"
"github.com/MicahParks/jwkset"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
)
func mustParseCIDR(v string) net.IPNet {
_, ne, err := net.ParseCIDR(v)
if err != nil {
panic(err)
}
if ipv4 := ne.IP.To4(); ipv4 != nil {
return net.IPNet{IP: ipv4, Mask: ne.Mask[len(ne.Mask)-4 : len(ne.Mask)]}
}
return *ne
}
func TestAuthInternal(t *testing.T) {
for _, outcome := range []string{
"ok",
"wrong user",
"wrong pass",
"wrong ip",
"wrong action",
"wrong path",
} {
for _, encryption := range []string{
"plain",
"sha256",
"argon2",
} {
t.Run(outcome+" "+encryption, func(t *testing.T) {
m := Manager{
Method: conf.AuthMethodInternal,
InternalUsers: []conf.AuthInternalUser{
{
IPs: conf.IPNetworks{mustParseCIDR("127.1.1.1/32")},
Permissions: []conf.AuthInternalUserPermission{{
Action: conf.AuthActionPublish,
Path: "mypath",
}},
},
},
}
switch encryption {
case "plain":
m.InternalUsers[0].User = conf.Credential("testuser")
m.InternalUsers[0].Pass = conf.Credential("testpass")
case "sha256":
m.InternalUsers[0].User = conf.Credential("sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ=")
m.InternalUsers[0].Pass = conf.Credential("sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w=")
case "argon2":
m.InternalUsers[0].User = conf.Credential(
"argon2:$argon2id$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$Ux/LWeTgJQPyfMMJo1myR64+o8rALHoPmlE1i/TR+58")
m.InternalUsers[0].Pass = conf.Credential(
"argon2:$argon2i$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$/mrZ42TiTv1mcPnpMUera5oi0SFYbbyueAbdx5sUvWo")
}
var req *Request
switch outcome {
case "ok":
req = &Request{
Action: conf.AuthActionPublish,
Path: "mypath",
Credentials: &Credentials{
User: "testuser",
Pass: "testpass",
},
IP: net.ParseIP("127.1.1.1"),
}
case "wrong user":
req = &Request{
Action: conf.AuthActionPublish,
Path: "mypath",
Credentials: &Credentials{
User: "wrong",
Pass: "testpass",
},
IP: net.ParseIP("127.1.1.1"),
}
case "wrong pass":
req = &Request{
Action: conf.AuthActionPublish,
Path: "mypath",
Credentials: &Credentials{
User: "testuser",
Pass: "wrong",
},
IP: net.ParseIP("127.1.1.1"),
}
case "wrong ip":
req = &Request{
Action: conf.AuthActionPublish,
Path: "mypath",
Credentials: &Credentials{
User: "testuser",
Pass: "testpass",
},
IP: net.ParseIP("127.1.1.2"),
}
case "wrong action":
req = &Request{
Action: conf.AuthActionRead,
Path: "mypath",
Credentials: &Credentials{
User: "testuser",
Pass: "testpass",
},
IP: net.ParseIP("127.1.1.1"),
}
case "wrong path":
req = &Request{
Action: conf.AuthActionPublish,
Path: "wrong",
Credentials: &Credentials{
User: "testuser",
Pass: "testpass",
},
IP: net.ParseIP("127.1.1.1"),
}
}
// first request with empty credentials
err := m.Authenticate(&Request{
Action: req.Action,
Path: req.Path,
Credentials: &Credentials{},
IP: req.IP,
})
require.Equal(t, &Error{
Wrapped: err.Wrapped,
AskCredentials: true,
}, err)
// second request
err = m.Authenticate(req)
if outcome == "ok" {
require.Nil(t, err)
} else {
require.EqualError(t, err.Wrapped, "authentication failed")
require.False(t, err.AskCredentials)
}
})
}
}
}
func TestAuthInternalCustomVerifyFunc(t *testing.T) {
for _, ca := range []string{"ok", "invalid"} {
t.Run(ca, func(t *testing.T) {
m := Manager{
Method: conf.AuthMethodInternal,
InternalUsers: []conf.AuthInternalUser{
{
User: "myuser",
Pass: "mypass",
IPs: conf.IPNetworks{mustParseCIDR("127.1.1.1/32")},
Permissions: []conf.AuthInternalUserPermission{{
Action: conf.AuthActionPublish,
Path: "mypath",
}},
},
},
}
req1 := &Request{
Action: conf.AuthActionPublish,
Path: "mypath",
Credentials: &Credentials{},
IP: net.ParseIP("127.1.1.1"),
CustomVerifyFunc: func(expectedUser, expectedPass string) bool {
require.Equal(t, "myuser", expectedUser)
require.Equal(t, "mypass", expectedPass)
return (ca == "ok")
},
}
err := m.Authenticate(req1)
if ca == "ok" {
require.Nil(t, err)
} else {
require.EqualError(t, err.Wrapped, "authentication failed")
}
})
}
}
func TestAuthHTTP(t *testing.T) {
for _, outcome := range []string{"ok", "fail"} {
t.Run(outcome, func(t *testing.T) {
firstReceived := false
httpServ := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "/auth", r.URL.Path)
var in struct {
IP string `json:"ip"`
User string `json:"user"`
Password string `json:"password"`
Path string `json:"path"`
Protocol string `json:"protocol"`
ID string `json:"id"`
Action string `json:"action"`
Query string `json:"query"`
}
err := json.NewDecoder(r.Body).Decode(&in)
require.NoError(t, err)
if in.IP != "127.0.0.1" ||
in.User != "testpublisher" ||
in.Password != "testpass" ||
in.Path != "teststream" ||
in.Protocol != "rtsp" ||
(firstReceived && in.ID == "") ||
in.Action != "publish" ||
(in.Query != "user=testreader&pass=testpass&param=value" &&
in.Query != "user=testpublisher&pass=testpass&param=value" &&
in.Query != "param=value") {
w.WriteHeader(http.StatusBadRequest)
return
}
firstReceived = true
}),
}
ln, err := net.Listen("tcp", "127.0.0.1:9120")
require.NoError(t, err)
go httpServ.Serve(ln)
defer httpServ.Shutdown(context.Background())
m := Manager{
Method: conf.AuthMethodHTTP,
HTTPAddress: "http://127.0.0.1:9120/auth",
}
var req *Request
if outcome == "ok" {
req = &Request{
Action: conf.AuthActionPublish,
Path: "teststream",
Query: "param=value",
Protocol: ProtocolRTSP,
Credentials: &Credentials{
User: "testpublisher",
Pass: "testpass",
},
IP: net.ParseIP("127.0.0.1"),
}
} else {
req = &Request{
Action: conf.AuthActionPublish,
Path: "teststream",
Query: "param=value",
Protocol: ProtocolRTSP,
Credentials: &Credentials{
User: "invalid",
Pass: "testpass",
},
IP: net.ParseIP("127.0.0.1"),
}
}
// first request with empty credentials
err2 := m.Authenticate(&Request{
Action: req.Action,
Path: req.Path,
Credentials: &Credentials{},
IP: req.IP,
})
require.Equal(t, &Error{
Wrapped: err2.Wrapped,
AskCredentials: true,
}, err2)
// second request
err2 = m.Authenticate(req)
if outcome == "ok" {
require.Nil(t, err2)
} else {
require.EqualError(t, err2.Wrapped, "server replied with code 400")
require.False(t, err2.AskCredentials)
}
})
}
}
func TestAuthHTTPExclude(t *testing.T) {
m := Manager{
Method: conf.AuthMethodHTTP,
HTTPAddress: "http://not-to-be-used:9120/auth",
HTTPExclude: []conf.AuthInternalUserPermission{{
Action: conf.AuthActionPublish,
}},
}
err := m.Authenticate(&Request{
Action: conf.AuthActionPublish,
Path: "teststream",
Query: "param=value",
Protocol: ProtocolRTSP,
Credentials: &Credentials{
User: "",
Pass: "",
},
IP: net.ParseIP("127.0.0.1"),
})
require.Nil(t, err)
}
func TestAuthJWT(t *testing.T) {
for _, ca := range []string{"object", "string"} {
t.Run(ca, func(t *testing.T) {
// reference:
// https://github.com/MicahParks/jwkset/blob/master/examples/http_server/main.go
key, err := rsa.GenerateKey(rand.Reader, 1024)
require.NoError(t, err)
httpServ := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jwk, err2 := jwkset.NewJWKFromKey(key, jwkset.JWKOptions{
Metadata: jwkset.JWKMetadataOptions{
KID: "test-key-id",
},
})
require.NoError(t, err2)
jwkSet := jwkset.NewMemoryStorage()
err2 = jwkSet.KeyWrite(context.Background(), jwk)
require.NoError(t, err2)
response, err2 := jwkSet.JSONPublic(r.Context())
if err2 != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(response)
}),
}
ln, err := net.Listen("tcp", "localhost:4567")
require.NoError(t, err)
go httpServ.Serve(ln)
defer httpServ.Shutdown(context.Background())
var req *Request
if ca == "object" {
type customClaims struct {
jwt.RegisteredClaims
MediaMTXPermissions string `json:"my_permission_key"`
}
var enc []byte
enc, err = json.Marshal([]conf.AuthInternalUserPermission{{
Action: conf.AuthActionPublish,
Path: "mypath",
}})
require.NoError(t, err)
claims := customClaims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "test",
Subject: "somebody",
ID: "1",
},
MediaMTXPermissions: string(enc),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header[jwkset.HeaderKID] = "test-key-id"
var ss string
ss, err = token.SignedString(key)
require.NoError(t, err)
req = &Request{
Action: conf.AuthActionPublish,
Path: "mypath",
Query: "param=value",
Protocol: ProtocolRTSP,
Credentials: &Credentials{
Token: ss,
},
IP: net.ParseIP("127.0.0.1"),
}
} else {
type customClaims struct {
jwt.RegisteredClaims
MediaMTXPermissions []conf.AuthInternalUserPermission `json:"my_permission_key"`
}
claims := customClaims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "test",
Subject: "somebody",
ID: "1",
},
MediaMTXPermissions: []conf.AuthInternalUserPermission{{
Action: conf.AuthActionPublish,
Path: "mypath",
}},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header[jwkset.HeaderKID] = "test-key-id"
var ss string
ss, err = token.SignedString(key)
require.NoError(t, err)
req = &Request{
Action: conf.AuthActionPublish,
Path: "mypath",
Protocol: ProtocolWebRTC,
Credentials: &Credentials{
Token: ss,
},
IP: net.ParseIP("127.0.0.1"),
}
}
m := Manager{
Method: conf.AuthMethodJWT,
JWTJWKS: "http://localhost:4567/jwks",
JWTClaimKey: "my_permission_key",
}
// first request with empty credentials
err2 := m.Authenticate(&Request{
Action: req.Action,
Path: req.Path,
Credentials: &Credentials{},
IP: req.IP,
})
require.Equal(t, &Error{
Wrapped: err2.Wrapped,
AskCredentials: true,
}, err2)
// second request
err2 = m.Authenticate(req)
require.Nil(t, err2)
})
}
}
func TestAuthJWTExclude(t *testing.T) {
m := Manager{
Method: conf.AuthMethodJWT,
JWTJWKS: "http://localhost:4567/jwks",
JWTClaimKey: "my_permission_key",
JWTExclude: []conf.AuthInternalUserPermission{{
Action: conf.AuthActionPublish,
}},
}
err := m.Authenticate(&Request{
Action: conf.AuthActionPublish,
Path: "teststream",
Query: "param=value",
Protocol: ProtocolRTSP,
IP: net.ParseIP("127.0.0.1"),
})
require.Nil(t, err)
}
func TestAuthJWTRefresh(t *testing.T) {
// reference:
// https://github.com/MicahParks/jwkset/blob/master/examples/http_server/main.go
var key *rsa.PrivateKey
httpServ := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jwk, err := jwkset.NewJWKFromKey(key, jwkset.JWKOptions{
Metadata: jwkset.JWKMetadataOptions{
KID: "test-key-id",
},
})
require.NoError(t, err)
jwkSet := jwkset.NewMemoryStorage()
err = jwkSet.KeyWrite(context.Background(), jwk)
require.NoError(t, err)
response, err2 := jwkSet.JSONPublic(r.Context())
if err2 != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(response)
}),
}
ln, err := net.Listen("tcp", "localhost:4567")
require.NoError(t, err)
go httpServ.Serve(ln)
defer httpServ.Shutdown(context.Background())
m := Manager{
Method: conf.AuthMethodJWT,
JWTJWKS: "http://localhost:4567/jwks",
JWTClaimKey: "my_permission_key",
}
for i := 0; i < 2; i++ {
key, err = rsa.GenerateKey(rand.Reader, 1024)
require.NoError(t, err)
type customClaims struct {
jwt.RegisteredClaims
MediaMTXPermissions []conf.AuthInternalUserPermission `json:"my_permission_key"`
}
claims := customClaims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "test",
Subject: "somebody",
ID: "1",
},
MediaMTXPermissions: []conf.AuthInternalUserPermission{{
Action: conf.AuthActionPublish,
Path: "mypath",
}},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header[jwkset.HeaderKID] = "test-key-id"
var ss string
ss, err = token.SignedString(key)
require.NoError(t, err)
err2 := m.Authenticate(&Request{
Action: conf.AuthActionPublish,
Path: "mypath",
Query: "param=value",
Protocol: ProtocolRTSP,
Credentials: &Credentials{
Token: ss,
},
IP: net.ParseIP("127.0.0.1"),
})
require.Nil(t, err2)
m.RefreshJWTJWKS()
}
}