mirror of
https://github.com/aler9/rtsp-simple-server
synced 2026-04-22 23:17:11 +08:00
@@ -108,6 +108,8 @@ components:
|
||||
$ref: '#/components/schemas/AuthInternalUser'
|
||||
authHTTPAddress:
|
||||
type: string
|
||||
authHTTPFingerprint:
|
||||
type: string
|
||||
authHTTPExclude:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -121,6 +121,21 @@ authHTTPExclude:
|
||||
- action: pprof
|
||||
```
|
||||
|
||||
If the authentication server uses HTTPS and has a self-signed or invalid TLS certificate, you can provide the fingerprint of the certificate to validate it anyway:
|
||||
|
||||
```yml
|
||||
authMethod: http
|
||||
authHTTPAddress: https://myauthserver/auth
|
||||
authHTTPFingerprint: 33949e05fffb5ff3e8aa16f8213a6251b4d9363804ba53233c4da9a46d6f2739
|
||||
```
|
||||
|
||||
The fingerprint can be obtained with:
|
||||
|
||||
```sh
|
||||
openssl s_client -connect myauthserver:443 </dev/null 2>/dev/null | sed -n '/BEGIN/,/END/p' > server.crt
|
||||
openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':'
|
||||
```
|
||||
|
||||
### External JWT provider
|
||||
|
||||
Authentication can be delegated to an external identity server, that is capable of generating JWTs and provides a JWKS endpoint. With respect to the HTTP-based method, this has the advantage that the external server is contacted once, and not for every request, greatly improving performance. In order to use the JWT-based authentication method, set `authMethod` and `authJWTJWKS`:
|
||||
@@ -146,6 +161,22 @@ The JWT is expected to contain a claim, with a list of permissions in the same f
|
||||
}
|
||||
```
|
||||
|
||||
If the JWKS server uses TLS and has a self-signed or invalid TLS certificate, you can provide the fingerprint of the certificate to validate it anyway:
|
||||
|
||||
```yml
|
||||
authMethod: jwt
|
||||
authJWTJWKS: https://my_identity_server/jwks_endpoint
|
||||
authJWTJWKSFingerprint: 33949e05fffb5ff3e8aa16f8213a6251b4d9363804ba53233c4da9a46d6f2739
|
||||
authJWTClaimKey: mediamtx_permissions
|
||||
```
|
||||
|
||||
The fingerprint can be obtained with:
|
||||
|
||||
```sh
|
||||
openssl s_client -connect my_identity_server:443 </dev/null 2>/dev/null | sed -n '/BEGIN/,/END/p' > server.crt
|
||||
openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':'
|
||||
```
|
||||
|
||||
#### Keycloak setup
|
||||
|
||||
Here's a tutorial on how to setup the [Keycloak identity server](https://www.keycloak.org/) in order to provide JWTs.
|
||||
|
||||
@@ -60,6 +60,7 @@ type Manager struct {
|
||||
Method conf.AuthMethod
|
||||
InternalUsers []conf.AuthInternalUser
|
||||
HTTPAddress string
|
||||
HTTPFingerprint string
|
||||
HTTPExclude []conf.AuthInternalUserPermission
|
||||
JWTJWKS string
|
||||
JWTJWKSFingerprint string
|
||||
@@ -172,7 +173,22 @@ func (m *Manager) authenticateHTTP(req *Request) error {
|
||||
Query: req.Query,
|
||||
})
|
||||
|
||||
res, err := http.Post(m.HTTPAddress, "application/json", bytes.NewReader(enc))
|
||||
u, err := url.Parse(m.HTTPAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: tls.MakeConfig(u.Hostname(), m.HTTPFingerprint),
|
||||
}
|
||||
defer tr.CloseIdleConnections()
|
||||
|
||||
httpClient := &http.Client{
|
||||
Timeout: m.ReadTimeout,
|
||||
Transport: tr,
|
||||
}
|
||||
|
||||
res, err := httpClient.Post(m.HTTPAddress, "application/json", bytes.NewReader(enc))
|
||||
if err != nil {
|
||||
return fmt.Errorf("HTTP request failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -16,6 +17,58 @@ import (
|
||||
"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 {
|
||||
@@ -309,6 +362,62 @@ func TestAuthHTTP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func TestAuthHTTPExclude(t *testing.T) {
|
||||
m := Manager{
|
||||
Method: conf.AuthMethodHTTP,
|
||||
@@ -584,3 +693,86 @@ func TestAuthJWTRefresh(t *testing.T) {
|
||||
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",
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -262,6 +262,7 @@ type Conf struct {
|
||||
AuthInternalUsers []AuthInternalUser `json:"authInternalUsers"`
|
||||
AuthHTTPAddress string `json:"authHTTPAddress"`
|
||||
ExternalAuthenticationURL *string `json:"externalAuthenticationURL,omitempty"` // deprecated
|
||||
AuthHTTPFingerprint string `json:"authHTTPFingerprint"`
|
||||
AuthHTTPExclude []AuthInternalUserPermission `json:"authHTTPExclude"`
|
||||
AuthJWTJWKS string `json:"authJWTJWKS"`
|
||||
AuthJWTJWKSFingerprint string `json:"authJWTJWKSFingerprint"`
|
||||
|
||||
@@ -337,6 +337,7 @@ func (p *Core) createResources(initial bool) error {
|
||||
Method: p.conf.AuthMethod,
|
||||
InternalUsers: p.conf.AuthInternalUsers,
|
||||
HTTPAddress: p.conf.AuthHTTPAddress,
|
||||
HTTPFingerprint: p.conf.AuthHTTPFingerprint,
|
||||
HTTPExclude: p.conf.AuthHTTPExclude,
|
||||
JWTJWKS: p.conf.AuthJWTJWKS,
|
||||
JWTJWKSFingerprint: p.conf.AuthJWTJWKSFingerprint,
|
||||
@@ -719,6 +720,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
||||
closeAuthManager := newConf == nil ||
|
||||
newConf.AuthMethod != p.conf.AuthMethod ||
|
||||
newConf.AuthHTTPAddress != p.conf.AuthHTTPAddress ||
|
||||
newConf.AuthHTTPFingerprint != p.conf.AuthHTTPFingerprint ||
|
||||
!reflect.DeepEqual(newConf.AuthHTTPExclude, p.conf.AuthHTTPExclude) ||
|
||||
newConf.AuthJWTJWKS != p.conf.AuthJWTJWKS ||
|
||||
newConf.AuthJWTJWKSFingerprint != p.conf.AuthJWTJWKSFingerprint ||
|
||||
|
||||
@@ -107,6 +107,12 @@ authInternalUsers:
|
||||
# If the response code is 20x, authentication is accepted, otherwise
|
||||
# it is discarded.
|
||||
authHTTPAddress:
|
||||
# If the HTTP authentication URL has a self-signed or invalid certificate,
|
||||
# you can provide the fingerprint of the certificate in order to
|
||||
# validate it anyway. It can be obtained by running:
|
||||
# openssl s_client -connect auth_http_domain:443 </dev/null 2>/dev/null | sed -n '/BEGIN/,/END/p' > server.crt
|
||||
# openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':'
|
||||
authHTTPFingerprint:
|
||||
# Actions to exclude from HTTP-based authentication.
|
||||
# Format is the same as the one of user permissions.
|
||||
authHTTPExclude:
|
||||
|
||||
Reference in New Issue
Block a user