mirror of
https://github.com/aler9/rtsp-simple-server
synced 2026-04-22 15:07:19 +08:00
use "token" as query parameter key to pass tokens (#5647)
the legacy "jwt" query parameter key is still supported.
This commit is contained in:
@@ -247,7 +247,7 @@ Here's a tutorial on how to setup the [Keycloak identity server](https://www.key
|
||||
http://localhost:8080/realms/mediamtx/protocol/openid-connect/token
|
||||
```
|
||||
|
||||
The JWT is inside the `access_token` key of the response:
|
||||
This results in:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -262,6 +262,8 @@ Here's a tutorial on how to setup the [Keycloak identity server](https://www.key
|
||||
}
|
||||
```
|
||||
|
||||
The JWT is inside the `access_token` key.
|
||||
|
||||
## Providing username and password
|
||||
|
||||
### RTSP
|
||||
@@ -285,7 +287,7 @@ rtmp://localhost/mystream?user=myuser&pass=mypass
|
||||
Append username and password to `streamid`:
|
||||
|
||||
```
|
||||
srt://localhost:8890?streamid=publish:mystream:user:pass&pkt_size=1316
|
||||
srt://localhost:8890?streamid=publish:mystream:myuser:mypass&pkt_size=1316
|
||||
```
|
||||
|
||||
### HLS and WebRTC
|
||||
@@ -293,7 +295,7 @@ srt://localhost:8890?streamid=publish:mystream:user:pass&pkt_size=1316
|
||||
Username and password can be passed through the `Authorization: Basic` HTTP header:
|
||||
|
||||
```
|
||||
Authorization: Basic base64(user:pass)
|
||||
Authorization: Basic base64(myuser:mypass)
|
||||
```
|
||||
|
||||
When using a web browser, a dialog is first shown to users, asking for credentials, and then the header is automatically inserted into every request. If you need to automatically fill credentials from a parent web page, read [Embed streams in a website](17-embed-streams-in-a-website.md).
|
||||
@@ -301,7 +303,7 @@ When using a web browser, a dialog is first shown to users, asking for credentia
|
||||
If the `Authorization: Basic` header cannot be used (for instance, in software like OBS Studio, which only allows to provide a "Bearer Token"), credentials can be passed through the `Authorization: Bearer` header (i.e. the "Bearer Token" in OBS), where the value is the concatenation of username and password, separated by a colon:
|
||||
|
||||
```
|
||||
Authorization: Bearer username:password
|
||||
Authorization: Bearer myuser:mypass
|
||||
```
|
||||
|
||||
## Providing tokens / JWTs
|
||||
@@ -311,7 +313,7 @@ Authorization: Bearer username:password
|
||||
Pass the token as a query parameter:
|
||||
|
||||
```
|
||||
rtsp://localhost:8554/mystream?jwt=jwt
|
||||
rtsp://localhost:8554/mystream?token=mytoken
|
||||
```
|
||||
|
||||
WARNING: FFmpeg implementation of RTSP does not support URLs that are longer than 4096 characters (this is the [MAX_URL_SIZE constant](https://github.com/FFmpeg/FFmpeg/blob/f951aa9ef382d6bb517e05d04d52710f751de427/libavformat/internal.h#L30)), therefore you have to configure your identity server in order to produce JWTs that are shorter than this threshold.
|
||||
@@ -321,7 +323,7 @@ WARNING: FFmpeg implementation of RTSP does not support URLs that are longer tha
|
||||
Pass the token as a query parameter:
|
||||
|
||||
```
|
||||
rtmp://localhost/mystream?jwt=jwt
|
||||
rtmp://localhost/mystream?token=mytoken
|
||||
```
|
||||
|
||||
WARNING: FFmpeg implementation of RTMP does not support URLs that are longer than 1024 characters (this is the [TCURL_MAX_LENGTH constant](https://github.com/FFmpeg/FFmpeg/blob/f951aa9ef382d6bb517e05d04d52710f751de427/libavformat/rtmpproto.c#L55)), therefore you have to configure your identity server in order to produce JWTs that are shorter than this threshold.
|
||||
@@ -331,7 +333,7 @@ WARNING: FFmpeg implementation of RTMP does not support URLs that are longer tha
|
||||
Pass the token as password, with an arbitrary user:
|
||||
|
||||
```
|
||||
srt://localhost:8890?streamid=publish:mystream:user:jwt&pkt_size=1316
|
||||
srt://localhost:8890?streamid=publish:mystream:user:mytoken&pkt_size=1316
|
||||
```
|
||||
|
||||
WARNING: SRT does not support Stream IDs that are longer than 512 characters, therefore you have to configure your identity server in order to produce JWTs that are shorter than this threshold.
|
||||
@@ -341,7 +343,7 @@ WARNING: SRT does not support Stream IDs that are longer than 512 characters, th
|
||||
The token can be passed through the `Authorization: Bearer` header:
|
||||
|
||||
```
|
||||
Authorization: Bearer MY_JWT
|
||||
Authorization: Bearer mytoken
|
||||
```
|
||||
|
||||
In OBS Studio, this is the "Bearer Token" field.
|
||||
|
||||
+29
-25
@@ -63,6 +63,32 @@ func matchesPermission(perms []conf.AuthInternalUserPermission, req *Request) bo
|
||||
return false
|
||||
}
|
||||
|
||||
func getToken(jwtInHTTPQuery bool, req *Request) (string, bool) {
|
||||
switch {
|
||||
case req.Credentials.Token != "":
|
||||
return req.Credentials.Token, true
|
||||
|
||||
case req.Credentials.Pass != "":
|
||||
return req.Credentials.Pass, true
|
||||
|
||||
// always allow passing tokens through query parameters with RTSP and RTMP since there's no alternative.
|
||||
case req.Protocol == ProtocolRTSP || req.Protocol == ProtocolRTMP || (jwtInHTTPQuery && isHTTP(req)):
|
||||
v, err := url.ParseQuery(req.Query)
|
||||
if err == nil {
|
||||
if len(v["token"]) == 1 {
|
||||
return v["token"][0], true
|
||||
}
|
||||
|
||||
// legacy query key
|
||||
if len(v["jwt"]) == 1 {
|
||||
return v["jwt"][0], true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Manager is the authentication manager.
|
||||
type Manager struct {
|
||||
Method conf.AuthMethod
|
||||
@@ -222,30 +248,8 @@ func (m *Manager) authenticateJWT(req *Request) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var encodedJWT string
|
||||
|
||||
switch {
|
||||
case req.Credentials.Token != "":
|
||||
encodedJWT = req.Credentials.Token
|
||||
|
||||
case req.Credentials.Pass != "":
|
||||
encodedJWT = req.Credentials.Pass
|
||||
|
||||
// always allow passing JWT through query parameters with RTSP and RTMP since there's no alternative.
|
||||
case req.Protocol == ProtocolRTSP || req.Protocol == ProtocolRTMP || (isHTTP(req) && m.JWTInHTTPQuery):
|
||||
var v url.Values
|
||||
v, err = url.ParseQuery(req.Query)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(v["jwt"]) != 1 || len(v["jwt"][0]) == 0 {
|
||||
return "", fmt.Errorf("JWT not provided")
|
||||
}
|
||||
|
||||
encodedJWT = v["jwt"][0]
|
||||
|
||||
default:
|
||||
token, ok := getToken(m.JWTInHTTPQuery, req)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("JWT not provided")
|
||||
}
|
||||
|
||||
@@ -259,7 +263,7 @@ func (m *Manager) authenticateJWT(req *Request) (string, error) {
|
||||
|
||||
var cc jwtClaims
|
||||
cc.permissionsKey = m.JWTClaimKey
|
||||
_, err = jwt.ParseWithClaims(encodedJWT, &cc, keyfunc, opts...)
|
||||
_, err = jwt.ParseWithClaims(token, &cc, keyfunc, opts...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -595,6 +595,89 @@ func TestAuthJWT(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ authJWTClaimKey: mediamtx_permissions
|
||||
# Actions to exclude from JWT-based authentication.
|
||||
# Format is the same as the one of user permissions.
|
||||
authJWTExclude: []
|
||||
# Allow passing the JWT through query parameters of HTTP requests (i.e. ?jwt=JWT).
|
||||
# Allow passing the JWT through query parameters of HTTP requests (i.e. ?token=JWT).
|
||||
# This is a security risk and will be disabled in the future.
|
||||
# RTSP and RTMP always allow JWT in query even if disabled, since there is no alternative.
|
||||
authJWTInHTTPQuery: true
|
||||
|
||||
Reference in New Issue
Block a user