mirror of
https://github.com/aler9/rtsp-simple-server
synced 2026-04-22 15:07:19 +08:00
4472bcfc4b
the legacy "jwt" query parameter key is still supported.
1096 lines
29 KiB
Go
1096 lines
29 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"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"
|
|
)
|
|
|
|
var testTLSCertPub = []byte(`-----BEGIN CERTIFICATE-----
|
|
MIIDazCCAlOgAwIBAgIUXw1hEC3LFpTsllv7D3ARJyEq7sIwDQYJKoZIhvcNAQEL
|
|
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
|
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDEyMTMxNzQ0NThaFw0zMDEy
|
|
MTExNzQ0NThaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
|
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
|
AQUAA4IBDwAwggEKAoIBAQDG8DyyS51810GsGwgWr5rjJK7OE1kTTLSNEEKax8Bj
|
|
zOyiaz8rA2JGl2VUEpi2UjDr9Cm7nd+YIEVs91IIBOb7LGqObBh1kGF3u5aZxLkv
|
|
NJE+HrLVvUhaDobK2NU+Wibqc/EI3DfUkt1rSINvv9flwTFu1qHeuLWhoySzDKEp
|
|
OzYxpFhwjVSokZIjT4Red3OtFz7gl2E6OAWe2qoh5CwLYVdMWtKR0Xuw3BkDPk9I
|
|
qkQKx3fqv97LPEzhyZYjDT5WvGrgZ1WDAN3booxXF3oA1H3GHQc4m/vcLatOtb8e
|
|
nI59gMQLEbnp08cl873bAuNuM95EZieXTHNbwUnq5iybAgMBAAGjUzBRMB0GA1Ud
|
|
DgQWBBQBKhJh8eWu0a4au9X/2fKhkFX2vjAfBgNVHSMEGDAWgBQBKhJh8eWu0a4a
|
|
u9X/2fKhkFX2vjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBj
|
|
3aCW0YPKukYgVK9cwN0IbVy/D0C1UPT4nupJcy/E0iC7MXPZ9D/SZxYQoAkdptdO
|
|
xfI+RXkpQZLdODNx9uvV+cHyZHZyjtE5ENu/i5Rer2cWI/mSLZm5lUQyx+0KZ2Yu
|
|
tEI1bsebDK30msa8QSTn0WidW9XhFnl3gRi4wRdimcQapOWYVs7ih+nAlSvng7NI
|
|
XpAyRs8PIEbpDDBMWnldrX4TP6EWYUi49gCp8OUDRREKX3l6Ls1vZ02F34yHIt/7
|
|
7IV/XSKG096bhW+icKBWV0IpcEsgTzPK1J1hMxgjhzIMxGboAeUU+kidthOob6Sd
|
|
XQxaORfgM//NzX9LhUPk
|
|
-----END CERTIFICATE-----
|
|
`)
|
|
|
|
var testTLSCertKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEogIBAAKCAQEAxvA8skudfNdBrBsIFq+a4ySuzhNZE0y0jRBCmsfAY8zsoms/
|
|
KwNiRpdlVBKYtlIw6/Qpu53fmCBFbPdSCATm+yxqjmwYdZBhd7uWmcS5LzSRPh6y
|
|
1b1IWg6GytjVPlom6nPxCNw31JLda0iDb7/X5cExbtah3ri1oaMkswyhKTs2MaRY
|
|
cI1UqJGSI0+EXndzrRc+4JdhOjgFntqqIeQsC2FXTFrSkdF7sNwZAz5PSKpECsd3
|
|
6r/eyzxM4cmWIw0+Vrxq4GdVgwDd26KMVxd6ANR9xh0HOJv73C2rTrW/HpyOfYDE
|
|
CxG56dPHJfO92wLjbjPeRGYnl0xzW8FJ6uYsmwIDAQABAoIBACi0BKcyQ3HElSJC
|
|
kaAao+Uvnzh4yvPg8Nwf5JDIp/uDdTMyIEWLtrLczRWrjGVZYbsVROinP5VfnPTT
|
|
kYwkfKINj2u+gC6lsNuPnRuvHXikF8eO/mYvCTur1zZvsQnF5kp4GGwIqr+qoPUP
|
|
bB0UMndG1PdpoMryHe+JcrvTrLHDmCeH10TqOwMsQMLHYLkowvxwJWsmTY7/Qr5S
|
|
Wm3PPpOcW2i0uyPVuyuv4yD1368fqnqJ8QFsQp1K6QtYsNnJ71Hut1/IoxK/e6hj
|
|
5Z+byKtHVtmcLnABuoOT7BhleJNFBksX9sh83jid4tMBgci+zXNeGmgqo2EmaWAb
|
|
agQslkECgYEA8B1rzjOHVQx/vwSzDa4XOrpoHQRfyElrGNz9JVBvnoC7AorezBXQ
|
|
M9WTHQIFTGMjzD8pb+YJGi3gj93VN51r0SmJRxBaBRh1ZZI9kFiFzngYev8POgD3
|
|
ygmlS3kTHCNxCK/CJkB+/jMBgtPj5ygDpCWVcTSuWlQFphePkW7jaaECgYEA1Blz
|
|
ulqgAyJHZaqgcbcCsI2q6m527hVr9pjzNjIVmkwu38yS9RTCgdlbEVVDnS0hoifl
|
|
+jVMEGXjF3xjyMvL50BKbQUH+KAa+V4n1WGlnZOxX9TMny8MBjEuSX2+362vQ3BX
|
|
4vOlX00gvoc+sY+lrzvfx/OdPCHQGVYzoKCxhLsCgYA07HcviuIAV/HsO2/vyvhp
|
|
xF5gTu+BqNUHNOZDDDid+ge+Jre2yfQLCL8VPLXIQW3Jff53IH/PGl+NtjphuLvj
|
|
7UDJvgvpZZuymIojP6+2c3gJ3CASC9aR3JBnUzdoE1O9s2eaoMqc4scpe+SWtZYf
|
|
3vzSZ+cqF6zrD/Rf/M35IQKBgHTU4E6ShPm09CcoaeC5sp2WK8OevZw/6IyZi78a
|
|
r5Oiy18zzO97U/k6xVMy6F+38ILl/2Rn31JZDVJujniY6eSkIVsUHmPxrWoXV1HO
|
|
y++U32uuSFiXDcSLarfIsE992MEJLSAynbF1Rsgsr3gXbGiuToJRyxbIeVy7gwzD
|
|
94TpAoGAY4/PejWQj9psZfAhyk5dRGra++gYRQ/gK1IIc1g+Dd2/BxbT/RHr05GK
|
|
6vwrfjsoRyMWteC1SsNs/CurjfQ/jqCfHNP5XPvxgd5Ec8sRJIiV7V5RTuWJsPu1
|
|
+3K6cnKEyg+0ekYmLertRFIY6SwWmY1fyKgTvxudMcsBY7dC4xs=
|
|
-----END RSA PRIVATE KEY-----
|
|
`)
|
|
|
|
func mustParseCIDR(v string) conf.IPNetwork {
|
|
_, ne, err := net.ParseCIDR(v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if ipv4 := ne.IP.To4(); ipv4 != nil {
|
|
return conf.IPNetwork{IP: ipv4, Mask: ne.Mask[len(ne.Mask)-4 : len(ne.Mask)]}
|
|
}
|
|
return conf.IPNetwork(*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
|
|
user, err := m.Authenticate(req)
|
|
if outcome == "ok" {
|
|
require.Nil(t, err)
|
|
require.Equal(t, "testuser", user)
|
|
} 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{
|
|
User: "myuser",
|
|
},
|
|
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")
|
|
},
|
|
}
|
|
|
|
user, err := m.Authenticate(req1)
|
|
if ca == "ok" {
|
|
require.Nil(t, err)
|
|
require.Equal(t, "myuser", user)
|
|
} 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¶m=value" &&
|
|
in.Query != "user=testpublisher&pass=testpass¶m=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
|
|
user, err2 := m.Authenticate(req)
|
|
if outcome == "ok" {
|
|
require.Nil(t, err2)
|
|
require.Equal(t, "testpublisher", user)
|
|
} else {
|
|
require.EqualError(t, err2.Wrapped, "server replied with code 400")
|
|
require.False(t, err2.AskCredentials)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthHTTPFingerprint(t *testing.T) {
|
|
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.User != "testuser" || in.Password != "testpass" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
}),
|
|
}
|
|
|
|
ln, err := net.Listen("tcp", "localhost:9121")
|
|
require.NoError(t, err)
|
|
|
|
cert, err := tls.X509KeyPair(testTLSCertPub, testTLSCertKey)
|
|
require.NoError(t, err)
|
|
|
|
httpServ.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
|
|
|
|
go httpServ.ServeTLS(ln, "", "")
|
|
defer httpServ.Shutdown(context.Background())
|
|
|
|
m := Manager{
|
|
Method: conf.AuthMethodHTTP,
|
|
HTTPAddress: "https://localhost:9121/auth",
|
|
HTTPFingerprint: "33949e05fffb5ff3e8aa16f8213a6251b4d9363804ba53233c4da9a46d6f2739",
|
|
}
|
|
|
|
user, err2 := m.Authenticate(&Request{
|
|
Action: conf.AuthActionPublish,
|
|
Path: "teststream",
|
|
Protocol: ProtocolRTSP,
|
|
Credentials: &Credentials{
|
|
User: "testuser",
|
|
Pass: "testpass",
|
|
},
|
|
IP: net.ParseIP("127.0.0.1"),
|
|
})
|
|
require.Nil(t, err2)
|
|
require.Equal(t, "testuser", user)
|
|
}
|
|
|
|
func TestAuthHTTPExclude(t *testing.T) {
|
|
m := Manager{
|
|
Method: conf.AuthMethodHTTP,
|
|
HTTPAddress: "http://not-to-be-used:9120/auth",
|
|
HTTPExclude: []conf.AuthInternalUserPermission{{
|
|
Action: conf.AuthActionPublish,
|
|
}},
|
|
}
|
|
|
|
user, 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)
|
|
require.Equal(t, "", user)
|
|
}
|
|
|
|
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
|
|
user, err2 := m.Authenticate(req)
|
|
require.Nil(t, err2)
|
|
require.Equal(t, "somebody", user)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthJWTQueryParameter(t *testing.T) {
|
|
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:4570")
|
|
require.NoError(t, err)
|
|
|
|
go httpServ.Serve(ln)
|
|
defer httpServ.Shutdown(context.Background())
|
|
|
|
for _, ca := range []string{"token", "jwt"} {
|
|
t.Run(ca, func(t *testing.T) {
|
|
type customClaims struct {
|
|
jwt.RegisteredClaims
|
|
MediaMTXPermissions []conf.AuthInternalUserPermission `json:"mediamtx_permissions"`
|
|
}
|
|
|
|
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"
|
|
ss, err2 := token.SignedString(key)
|
|
require.NoError(t, err2)
|
|
|
|
m := Manager{
|
|
Method: conf.AuthMethodJWT,
|
|
JWTJWKS: "http://localhost:4570/jwks",
|
|
JWTClaimKey: "mediamtx_permissions",
|
|
}
|
|
|
|
user, err2 := m.Authenticate(&Request{
|
|
Action: conf.AuthActionPublish,
|
|
Path: "mypath",
|
|
Query: ca + "=" + ss,
|
|
Protocol: ProtocolRTSP,
|
|
Credentials: &Credentials{
|
|
Token: "",
|
|
},
|
|
IP: net.ParseIP("127.0.0.1"),
|
|
})
|
|
require.Nil(t, err2)
|
|
require.Equal(t, "somebody", user)
|
|
})
|
|
}
|
|
}
|
|
|
|
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,
|
|
}},
|
|
}
|
|
|
|
user, 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)
|
|
require.Equal(t, "", user)
|
|
}
|
|
|
|
func TestAuthJWTIssuer(t *testing.T) {
|
|
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:4568")
|
|
require.NoError(t, err)
|
|
|
|
go httpServ.Serve(ln)
|
|
defer httpServ.Shutdown(context.Background())
|
|
|
|
for _, ca := range []struct {
|
|
name string
|
|
jwtIssuer string
|
|
tokenIss string
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "matching",
|
|
jwtIssuer: "my-issuer",
|
|
tokenIss: "my-issuer",
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "mismatched",
|
|
jwtIssuer: "my-issuer",
|
|
tokenIss: "wrong-issuer",
|
|
expectErr: true,
|
|
},
|
|
} {
|
|
t.Run(ca.name, func(t *testing.T) {
|
|
signToken := func(issuer string) string {
|
|
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: issuer,
|
|
},
|
|
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)
|
|
return ss
|
|
}
|
|
ss := signToken(ca.tokenIss)
|
|
|
|
m := Manager{
|
|
Method: conf.AuthMethodJWT,
|
|
JWTJWKS: "http://localhost:4568/jwks",
|
|
JWTClaimKey: "my_permission_key",
|
|
JWTIssuer: ca.jwtIssuer,
|
|
}
|
|
|
|
_, err := m.Authenticate(&Request{
|
|
Action: conf.AuthActionPublish,
|
|
Path: "mypath",
|
|
Protocol: ProtocolRTSP,
|
|
Credentials: &Credentials{
|
|
Token: ss,
|
|
},
|
|
IP: net.ParseIP("127.0.0.1"),
|
|
})
|
|
|
|
if ca.expectErr {
|
|
require.NotNil(t, err)
|
|
} else {
|
|
require.Nil(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthJWTAudience(t *testing.T) {
|
|
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:4569")
|
|
require.NoError(t, err)
|
|
|
|
go httpServ.Serve(ln)
|
|
defer httpServ.Shutdown(context.Background())
|
|
|
|
for _, ca := range []struct {
|
|
name string
|
|
jwtAudience string
|
|
tokenAud jwt.ClaimStrings
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "matching",
|
|
jwtAudience: "my-audience",
|
|
tokenAud: jwt.ClaimStrings{"my-audience"},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "mismatched",
|
|
jwtAudience: "my-audience",
|
|
tokenAud: jwt.ClaimStrings{"wrong-audience"},
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "present in list",
|
|
jwtAudience: "my-audience",
|
|
tokenAud: jwt.ClaimStrings{"other", "my-audience"},
|
|
expectErr: false,
|
|
},
|
|
} {
|
|
t.Run(ca.name, func(t *testing.T) {
|
|
signToken := func(audience jwt.ClaimStrings) string {
|
|
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()),
|
|
Audience: audience,
|
|
},
|
|
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)
|
|
return ss
|
|
}
|
|
ss := signToken(ca.tokenAud)
|
|
|
|
m := Manager{
|
|
Method: conf.AuthMethodJWT,
|
|
JWTJWKS: "http://localhost:4569/jwks",
|
|
JWTClaimKey: "my_permission_key",
|
|
JWTAudience: ca.jwtAudience,
|
|
}
|
|
|
|
_, err := m.Authenticate(&Request{
|
|
Action: conf.AuthActionPublish,
|
|
Path: "mypath",
|
|
Protocol: ProtocolRTSP,
|
|
Credentials: &Credentials{
|
|
Token: ss,
|
|
},
|
|
IP: net.ParseIP("127.0.0.1"),
|
|
})
|
|
|
|
if ca.expectErr {
|
|
require.NotNil(t, err)
|
|
} else {
|
|
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 range 2 {
|
|
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)
|
|
|
|
user, 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)
|
|
require.Equal(t, "somebody", user)
|
|
|
|
m.RefreshJWTJWKS()
|
|
}
|
|
}
|
|
|
|
func TestAuthJWTFingerprint(t *testing.T) {
|
|
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:4568")
|
|
require.NoError(t, err)
|
|
|
|
cert, err := tls.X509KeyPair(testTLSCertPub, testTLSCertKey)
|
|
require.NoError(t, err)
|
|
|
|
httpServ.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
|
|
|
|
go httpServ.ServeTLS(ln, "", "")
|
|
defer httpServ.Shutdown(context.Background())
|
|
|
|
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"
|
|
ss, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
m := Manager{
|
|
Method: conf.AuthMethodJWT,
|
|
JWTJWKS: "https://localhost:4568/jwks",
|
|
JWTJWKSFingerprint: "33949e05fffb5ff3e8aa16f8213a6251b4d9363804ba53233c4da9a46d6f2739",
|
|
JWTClaimKey: "my_permission_key",
|
|
}
|
|
|
|
user, err2 := m.Authenticate(&Request{
|
|
Action: conf.AuthActionPublish,
|
|
Path: "mypath",
|
|
Protocol: ProtocolRTSP,
|
|
Credentials: &Credentials{
|
|
Token: ss,
|
|
},
|
|
IP: net.ParseIP("127.0.0.1"),
|
|
})
|
|
require.Nil(t, err2)
|
|
require.Equal(t, "somebody", user)
|
|
}
|