Repository: bluenviron/mediamtx

Issue:
JWT authentication token too long for SRT publish 
<!--
To increase the chance of your issue getting fixed, open an issue FOR EACH problem. Please do not report multiple problems in a single issue, otherwise they'll probably never get ALL fixed.

Please include all sections of this template into your issue, or it will be automatically closed.
-->

## Which version are you using?

v1.8.2

## Which operating system are you using?

<!-- fill checkboxes with a x. Example: [x] Linux -->

- [ ] Linux amd64 standard
- [ ] Linux amd64 Docker
- [ ] Linux arm64 standard
- [ ] Linux arm64 Docker
- [ ] Linux arm7 standard
- [ ] Linux arm7 Docker
- [ ] Linux arm6 standard
- [ ] Linux arm6 Docker
- [x] Windows amd64 standard
- [ ] Windows amd64 Docker (WSL backend)
- [ ] macOS amd64 standard
- [ ] macOS amd64 Docker
- [ ] Other (please describe)

## Describe the issue

In order to send the JWT token using SRT when JWT authentication is enabled, the JWT token is a part of the stream ID:
srt://x.x.x.x:8890?streamid=publish:mystream:jwt=[token]

The issue with this implementation is that the streamid may only contain 512 characters. We created a token with almost the bare minimum but ended up with 685 characters:
https://github.com/Haivision/srt/blob/master/docs/features/access-control.md

So technically this is not a bug and this is SRT related. But this leaves us with an implementation that is unusable, or is there another way to pass JWT tokens?

## Describe how to replicate the issue

<!--
the maintainers must be able to REPLICATE your issue to solve it - therefore, describe in a very detailed way how to replicate it.
-->

1. start the server
2. publish with ...
```
./srt-live-transmit udp://:5004 "srt://192.168.5.99:8890?streamid=publish:mystream:jwt=[token]" -v -s 100 -ll debug
Media path: 'udp://:5004' --> 'srt://192.168.5.99:8890?streamid=publish:mystream:jwt=[token]'
SRT parameters specified:

        streamid = 'publish:mystream:jwt=[token]'
Opening SRT target caller on 192.168.5.99:8890
16:52:24.463124/srt-live-transm D:SRT.sm: generateSocketID: : @310547703
WARNING: failed to set options: streamid,

ERROR #5003.0: Operation not supported: Bad parameters
SrtCommon: DESTROYING CONNECTION, closing sockets (rt%310547703 ls%-1)...
SrtCommon: ... done.
ERROR: error: ConfigurePre: Operation not supported: Bad parameters
```

## Did you attach the server logs?

<!--
Server logs are sometimes useful to identify the issue.
If you think this is the case, set the parameter 'logLevel' to 'debug' and attach the server logs.
-->

no

## Did you attach a network dump?

<!--
If the bug arises when using MediaMTX with an external hardware or software, the most helpful content you can provide is a dump of the data exchanged between the server and the target (network dump), that can be generated in this way:
1) Download wireshark (https://www.wireshark.org/)
2) Start capturing on the interface used for exchanging packets (if the server and the external hardware or software are both installed on your pc, the interface is probably "loopback", otherwise it's the one of your network card)
3) Start the server and replicate the issue
4) Stop capturing, save the result in .pcap format
5) Attach
-->

no


Discussion:
@aler9: Hello, your analysis is right. Although the server tries to provide a protocol-independent set of features, sometimes it happens that a specific feature is not compatible with a certain protocol. JWTs are the standard way to authenticate requests without continuously polling an identity server, and supporting them has provided lots of benefits, but evidently they cannot be used with SRT. There isn't an easy solution for this.@jquiroga-arg: Hi, I implemented a workaround to address JWT token length limitations present in several clients, such as FFmpeg. This approach achieves around a 30% reduction in token size. What I do is compress the token using Gzip, and then decompress it from MediaMTX.

`diff --git a/internal/auth/manager.go b/internal/auth/manager.go
index 8a9f9aac..720f40c1 100644
--- a/internal/auth/manager.go
+++ b/internal/auth/manager.go
@@ -13,6 +13,9 @@ import (
 	"sync"
 	"time"
 
+	"compress/gzip"
+	"encoding/base64"
+
 	"github.com/MicahParks/keyfunc/v3"
 	"github.com/bluenviron/mediamtx/internal/conf"
 	"github.com/bluenviron/mediamtx/internal/protocols/tls"
@@ -254,7 +257,14 @@ func (m *Manager) authenticateJWT(req *Request) error {
 
 	var cc jwtClaims
 	cc.permissionsKey = m.JWTClaimKey
-	_, err = jwt.ParseWithClaims(encodedJWT, &cc, keyfunc)
+
+	//FIX JWT @ jquiroga
+	decodedJWT, err := DecompressBase64Gzip(encodedJWT)
+	if err != nil {
+		return fmt.Errorf("Error Decompressing JWT Token %w", err.Error())
+	}
+
+	_, err = jwt.ParseWithClaims(decodedJWT, &cc, keyfunc)
 	if err != nil {
 		return err
 	}
@@ -314,3 +324,37 @@ func (m *Manager) RefreshJWTJWKS() {
 
 	m.jwksLastRefresh = time.Time{}
 }
+
+func DecompressBase64Gzip(input string) (string, error) {
+	unescaped := strings.ReplaceAll(input, " ", "+")
+
+	/*unescaped, err := url.QueryUnescape(input)
+	if err != nil {
+		return "", fmt.Errorf("error unescaping URL: %w", err)
+	}*/
+
+	// Decode Base64 string
+	compressedData, err := base64.StdEncoding.DecodeString(unescaped)
+	if err != nil {
+		return "", fmt.Errorf("error decoding base64: %w", err)
+	}
+
+	// Create a GZIP reader
+	reader, err := gzip.NewReader(bytes.NewReader(compressedData))
+	if err != nil {
+		return "", fmt.Errorf("error creating gzip reader: %w", err)
+	}
+	defer reader.Close()
+
+	// Read the decompressed data
+	var uncompressed bytes.Buffer
+	_, err = io.Copy(&uncompressed, reader)
+	if err != nil {
+		return "", fmt.Errorf("error reading gzip data: %w", err)
+	}
+
+	// Convert decompressed bytes back to base64
+	originalBase64 := uncompressed.String()
+
+	return originalBase64, nil
+}
`@aler9: @jquiroga-arg although this is a good idea, a compressed JWT might still be longer than the maximum length, so what you posted is a specific solution and not a general one. At the moment, using IDs and one of the other authentication methods (internal / HTTP external) is the only reliable way to perform SRT authentication.@aler9: The [Documentation](https://mediamtx.org/docs/usage/authentication) has been updated in order to advertise the limitation discussed here.

Just a clarification: it's perfectly possible to authenticate SRT clients with JWTs, but JWTs have to be shorter than 512 bytes (something less, since the limit is applied to the entire streamid string). Therefore, you have to configure your identity server in order to generate such short JWTs.

There are similar limits that apply to RTSP and RTMP, that are also reported in the documentation.

Solution patch:
diff --git a/internal/auth/manager.go b/internal/auth/manager.go
index 649d009b..f8392fa2 100644
--- a/internal/auth/manager.go
+++ b/internal/auth/manager.go
@@ -106,6 +106,7 @@ type Manager struct {
 	HTTPExclude   []conf.AuthInternalUserPermission
 	JWTJWKS       string
 	JWTClaimKey   string
+	JWTExclude    []conf.AuthInternalUserPermission
 	ReadTimeout   time.Duration
 
 	mutex          sync.RWMutex
@@ -229,6 +230,10 @@ func (m *Manager) authenticateHTTP(req *Request) error {
 }
 
 func (m *Manager) authenticateJWT(req *Request) error {
+	if matchesPermission(m.JWTExclude, req) {
+		return nil
+	}
+
 	keyfunc, err := m.pullJWTJWKS()
 	if err != nil {
 		return err
diff --git a/internal/conf/conf.go b/internal/conf/conf.go
index d3a0e747..2ac2809c 100644
--- a/internal/conf/conf.go
+++ b/internal/conf/conf.go
@@ -179,6 +179,7 @@ type Conf struct {
 	AuthHTTPExclude           AuthInternalUserPermissions `json:"authHTTPExclude"`
 	AuthJWTJWKS               string                      `json:"authJWTJWKS"`
 	AuthJWTClaimKey           string                      `json:"authJWTClaimKey"`
+	AuthJWTExclude            AuthInternalUserPermissions `json:"authJWTExclude"`
 
 	// Control API
 	API               bool       `json:"api"`
@@ -332,6 +333,7 @@ func (conf *Conf) setDefaults() {
 		},
 	}
 	conf.AuthJWTClaimKey = "mediamtx_permissions"
+	conf.AuthJWTExclude = []AuthInternalUserPermission{}
 
 	// Control API
 	conf.APIAddress = ":9997"
diff --git a/internal/core/core.go b/internal/core/core.go
index 0a168c71..4adbdb63 100644
--- a/internal/core/core.go
+++ b/internal/core/core.go
@@ -275,6 +275,7 @@ func (p *Core) createResources(initial bool) error {
 			HTTPExclude:   p.conf.AuthHTTPExclude,
 			JWTJWKS:       p.conf.AuthJWTJWKS,
 			JWTClaimKey:   p.conf.AuthJWTClaimKey,
+			JWTExclude:    p.conf.AuthJWTExclude,
 			ReadTimeout:   time.Duration(p.conf.ReadTimeout),
 		}
 	}
@@ -659,6 +660,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
 		!reflect.DeepEqual(newConf.AuthHTTPExclude, p.conf.AuthHTTPExclude) ||
 		newConf.AuthJWTJWKS != p.conf.AuthJWTJWKS ||
 		newConf.AuthJWTClaimKey != p.conf.AuthJWTClaimKey ||
+		!reflect.DeepEqual(newConf.AuthJWTExclude, p.conf.AuthJWTExclude) ||
 		newConf.ReadTimeout != p.conf.ReadTimeout
 	if !closeAuthManager && !reflect.DeepEqual(newConf.AuthInternalUsers, p.conf.AuthInternalUsers) {
 		p.authManager.ReloadInternalUsers(newConf.AuthInternalUsers)
diff --git a/mediamtx.yml b/mediamtx.yml
index 7c37e665..0342852f 100644
--- a/mediamtx.yml
+++ b/mediamtx.yml
@@ -125,6 +125,9 @@ authHTTPExclude:
 authJWTJWKS:
 # name of the claim that contains permissions.
 authJWTClaimKey: mediamtx_permissions
+# Actions to exclude from JWT-based authentication.
+# Format is the same as the one of user permissions.
+authJWTExclude: []
 
 ###############################################
 # Global settings -> Control API



Test patch:
diff --git a/internal/auth/manager_test.go b/internal/auth/manager_test.go
index be95ac26..1c4e0b7d 100644
--- a/internal/auth/manager_test.go
+++ b/internal/auth/manager_test.go
@@ -412,3 +412,25 @@ func TestAuthJWT(t *testing.T) {
 		})
 	}
 }
+
+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{
+		User:     "",
+		Pass:     "",
+		IP:       net.ParseIP("127.0.0.1"),
+		Action:   conf.AuthActionPublish,
+		Path:     "teststream",
+		Protocol: ProtocolRTSP,
+		Query:    "param=value",
+	})
+	require.NoError(t, err)
+}