Skip to content

Commit

Permalink
webrtc: support passing username and password through Bearer Token (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
aler9 committed Jun 11, 2024
1 parent caa9fa6 commit e76f6d3
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 7 deletions.
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ _rtsp-simple-server_ has been rebranded as _MediaMTX_. The reason is pretty obvi
* [SRT-specific features](#srt-specific-features)
* [Standard stream ID syntax](#standard-stream-id-syntax)
* [WebRTC-specific features](#webrtc-specific-features)
* [Connectivity issues](#connectivity-issues)
* [Authenticating with WHIP/WHEP](#authenticating-with-whip/whep)
* [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues)
* [RTSP-specific features](#rtsp-specific-features)
* [Transport protocols](#transport-protocols)
* [Encryption](#encryption)
Expand Down Expand Up @@ -338,6 +339,7 @@ Latest versions of OBS Studio can publish to the server with the [WebRTC / WHIP

* Service: `WHIP`
* Server: `http://localhost:8889/mystream/whip`
* Bearer Token: `myuser:mypass` (if internal authentication is enabled) or JWT (if JWT-based authentication is enabled)

Save the configuration and click `Start streaming`.

Expand Down Expand Up @@ -610,7 +612,9 @@ WHIP is a WebRTC extensions that allows to publish streams by using a URL, witho
http://localhost:8889/mystream/whip
```

Depending on the network it may be difficult to establish a connection between server and clients, see [WebRTC-specific features](#webrtc-specific-features) for remediations.
Regarding authentication, read [Authenticating with WHIP/WHEP](#authenticating-with-whip/whep).

Depending on the network it may be difficult to establish a connection between server and clients, read [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues).

Known clients that can publish with WebRTC and WHIP are [FFmpeg](#ffmpeg), [GStreamer](#gstreamer), [OBS Studio](#obs-studio).

Expand Down Expand Up @@ -876,7 +880,9 @@ WHEP is a WebRTC extensions that allows to read streams by using a URL, without
http://localhost:8889/mystream/whep
```

Depending on the network it may be difficult to establish a connection between server and clients, see [WebRTC-specific features](#webrtc-specific-features) for remediations.
Regarding authentication, read [Authenticating with WHIP/WHEP](#authenticating-with-whip/whep).

Depending on the network it may be difficult to establish a connection between server and clients, read [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues).

Known clients that can read with WebRTC and WHEP are [FFmpeg](#ffmpeg-1), [GStreamer](#gstreamer-1) and [web browsers](#web-browsers-1).

Expand Down Expand Up @@ -1838,7 +1844,35 @@ Where:

### WebRTC-specific features

#### Connectivity issues
#### Authenticating with WHIP/WHEP

When using WHIP or WHEP to establish a WebRTC connection, there are multiple ways to provide credentials.

If internal authentication or HTTP-based authentication is enabled, username and password can be passed through the `Authentication: Basic` header:

```
Authentication: Basic [base64_encoded_credentials]
```

Username and password can be also passed through the `Authentication: Bearer` header (since it's mandated by the specification):

```
Authentication: Bearer username:password
```

If JWT-based authentication is enabled, JWT can be passed through the `Authentication: Bearer` header:

```
Authentication: Bearer [jwt]
```

The JWT can also be passed through query parameters:

```
http://localhost:8889/mystream/whip?jwt=[jwt]
```

#### Solving WebRTC connectivity issues

If the server is hosted inside a container or is behind a NAT, additional configuration is required in order to allow the two WebRTC parts (server and client) to establish a connection.

Expand Down
18 changes: 16 additions & 2 deletions internal/servers/webrtc/http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,17 @@ func (s *httpServer) close() {

func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool {
user, pass, hasCredentials := ctx.Request.BasicAuth()

q := ctx.Request.URL.RawQuery

if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
// JWT in authorization bearer -> JWT in query parameters

Check warning on line 127 in internal/servers/webrtc/http_server.go

View check run for this annotation

Codecov / codecov/patch

internal/servers/webrtc/http_server.go#L127

Added line #L127 was not covered by tests
q = addJWTFromAuthorization(q, h)

// credentials in authorization bearer -> credentials in authorization basic
if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 {
user = parts[0]
pass = parts[1]
}

Check warning on line 134 in internal/servers/webrtc/http_server.go

View check run for this annotation

Codecov / codecov/patch

internal/servers/webrtc/http_server.go#L129-L134

Added lines #L129 - L134 were not covered by tests
}

_, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
Expand Down Expand Up @@ -194,10 +201,17 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
}

user, pass, _ := ctx.Request.BasicAuth()

q := ctx.Request.URL.RawQuery

if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
// JWT in authorization bearer -> JWT in query parameters
q = addJWTFromAuthorization(q, h)

// credentials in authorization bearer -> credentials in authorization basic
if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 {
user = parts[0]
pass = parts[1]
}
}

res := s.parent.newSession(webRTCNewSessionReq{
Expand Down
81 changes: 80 additions & 1 deletion internal/servers/webrtc/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ func TestServerRead(t *testing.T) {
}
}

func TestServerReadAuthorizationHeader(t *testing.T) {
func TestServerReadAuthorizationBearerJWT(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}

str, err := stream.New(
Expand Down Expand Up @@ -680,6 +680,85 @@ func TestServerReadAuthorizationHeader(t *testing.T) {
require.Equal(t, http.StatusCreated, res.StatusCode)
}

func TestServerReadAuthorizationUserPass(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}

str, err := stream.New(
1460,
desc,
true,
test.NilLogger,
)
require.NoError(t, err)

path := &dummyPath{stream: str}

pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return path, str, nil
},
}

s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512,
LocalUDPAddress: "127.0.0.1:8887",
LocalTCPAddress: "127.0.0.1:8887",
IPsFromInterfaces: true,
IPsFromInterfacesList: []string{},
AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil,
PathManager: pm,
Parent: test.NilLogger,
}
err = s.Initialize()
require.NoError(t, err)
defer s.Close()

tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}

pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{})
require.NoError(t, err)
defer pc.Close() //nolint:errcheck

_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)
require.NoError(t, err)

offer, err := pc.CreateOffer(nil)
require.NoError(t, err)

req, err := http.NewRequest(http.MethodPost,
"http://localhost:8886/teststream/whep", bytes.NewReader([]byte(offer.SDP)))
require.NoError(t, err)

req.Header.Set("Content-Type", "application/sdp")
req.Header.Set("Authorization", "Bearer myuser:mypass")

res, err := hc.Do(req)
require.NoError(t, err)
defer res.Body.Close()

require.Equal(t, http.StatusCreated, res.StatusCode)
}

func TestServerReadNotFound(t *testing.T) {
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
Expand Down

0 comments on commit e76f6d3

Please sign in to comment.