Skip to content
This repository has been archived by the owner on Jul 17, 2024. It is now read-only.

Commit

Permalink
Merge pull request #9 from Roverr/feature/auth
Browse files Browse the repository at this point in the history
JWT Authentication
  • Loading branch information
Roverr authored Jan 31, 2019
2 parents d30ac12 + 1a780b4 commit 378424a
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 13 deletions.
9 changes: 9 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@
[[constraint]]
name = "github.com/brianvoe/gofakeit"
version = "3.15.0"

[[constraint]]
name = "github.com/dgrijalva/jwt-go"
version = "3.2.0"
73 changes: 61 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ rtsp-stream is an easy to use out of box solution that can be integrated into ex

## Table of contents
* [How does it work](https://github.com/Roverr/rtsp-stream#how-does-it-work)
* [Authentication](https://github.com/Roverr/rtsp-stream#authentication)
* [No Authentication](https://github.com/Roverr/rtsp-stream#no-authentication)
* [JWT](https://github.com/Roverr/rtsp-stream#jwt-authentication)
* [Easy API](https://github.com/Roverr/rtsp-stream#easy-api)
* [Configuration](https://github.com/Roverr/rtsp-stream#configuration)
* [Run with Docker](https://github.com/Roverr/rtsp-stream#run-with-docker)
Expand All @@ -19,11 +22,48 @@ It converts `RTSP` streams into `HLS` based on traffic. The idea behind this is

There's a running go routine in the background that checks if a stream is being active or not. If it's not the transcoding stops until the next request for that stream.

## Authentication

The application offers different ways for authentication. There are situations when you can get away with no authentication, just
trusting requests because they are from reliable sources or just because they know how to use the API. In other cases, production cases, you definitely
want to protect the service. This application was not written to handle users and logins, so authentication is as lightweight as possible.


### No Authentication

**By default there is no authentication** what so ever. This can be useful if you have private subnets
where there is no real way to reach the service from the internet. (So every request is kind of trusted.) Also works great
if you just wanna try it out, maybe for home use.


### JWT Authentication

You can use shared key JWT authentication for the service.

The service itself does not create any tokens, but your authentication service can create.
After it's created it can be validated in the transcoder using the same secret / keys.
It is the easiest way to integrate into existing systems.
The following environment variables are available for this setup:

* **RTSP_STREAM_AUTH_JWT_ENABLED** - bool (false by default) - Indicates if the service should use the JWT authentication for the requests
* **RTPS_STREAM_AUTH_JWT_SECRET** - string (macilaci by default) - The secret used for creating the JWT tokens
* **RTSP_STREAM_AUTH_JWT_PUB_PATH** - string (/key.pub by default) - Path to the public RSA key.
* **RTSP_STREAM_AUTH_JWT_METHOD** - string (secret by default) - Can be `secret` or `rsa`. Changes how the application does the JWT verification.

You won't need the private key for it because no signing happens in this application.

<img src="./transcoder_auth.png"/>

## Easy API
**There are 2 main endpoints to call:**

`POST /start`

Starts the transcoding of the given stream. You have to pass URI format with rtsp procotol.
The respond should be considered the subpath for the video player to call.
So if your applicaiton is `myapp.com` then you should call `myapp.com/stream/host/index.m3u8` in your video player.
The reason for this is to remain flexible regarding useability.

Requires payload:
```js
{ "uri": "rtsp://username:password@host" }
Expand All @@ -39,11 +79,17 @@ Response:

Simple static file serving which is used when fetching chunks of `HLS`. This will be called by the client (browser) to fetch the chunks of the stream based on the given `index.m3u8`
<hr>
And there is also a third one which can be used for debugging (but you have to enable it via env variable)

`GET /list`

Lists all streams that are stored in the system along with their state of running:
This (kind of a debug) endpoint is used to list the streams in the system.
Since the application does not handle users, it does not handle permissions obviously.
You might not want everyone to be able to list the streams
available in the system. But if you do, you can use this. You just have to enable it via [env variable](https://github.com/Roverr/rtsp-stream#configuration).



Response:
```js
[
{
Expand All @@ -52,17 +98,16 @@ Lists all streams that are stored in the system along with their state of runnin
}
]
```
<hr>

## Configuration

You can configure the following settings in the application with environment variables:

* `RTSP_STREAM_CLEANUP_TIME` - bool - Time period for the cleanup process [info on format here](https://golang.org/pkg/time/#ParseDuration) default: `2m0s`
* `RTSP_STREAM_STORE_DIR` - string - Sub directory to store video chunks
* `RTSP_STREAM_PORT` - number - Port where the application listens
* `RTSP_STREAM_DEBUG` - bool - Turns on / off debug logging
* `RTSP_STREAM_LIST_ENDPOINT` - bool - Turns on / off the `/list` endpoint
* `RTSP_STREAM_CLEANUP_TIME` - string (2m0s by default) - Time period for the cleanup process [info on format here](https://golang.org/pkg/time/#ParseDuration)
* `RTSP_STREAM_STORE_DIR` - string (./videos by default) - Sub directory to store video chunks
* `RTSP_STREAM_PORT` - number (8080 by default) - Port where the application listens
* `RTSP_STREAM_DEBUG` - bool (false by default) - Turns on / off debug logging
* `RTSP_STREAM_LIST_ENDPOINT` - bool (false by default) - Turns on / off the `/list` endpoint

**CORS related configuration:**

Expand Down Expand Up @@ -97,7 +142,11 @@ You should expect something like this:

## Coming soon features

* Proper logging - File logging for the output of ffmpeg with the option of rotating file log
* Improved cleanup - Unused streams should be removed from the system after a while
* Authentication layer - More options for creating authentication within the service
* API improvements - Delete endpoint for streams so clients can remove streams whenever they would like to
✅ - Done

🤷‍♂️ - Needs more labour

* 🤷‍♂️ Proper logging - File logging for the output of ffmpeg with the option of rotating file log
* 🤷‍♂️ Improved cleanup - Unused streams should be removed from the system after a while
* 🤷‍♂️ API improvements - Delete endpoint for streams so clients can remove streams whenever they would like to
* ✅ Authentication layer - More options for creating authentication within the service
66 changes: 66 additions & 0 deletions core/auth/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package auth

import (
"crypto/rsa"
"fmt"
"io/ioutil"
"strings"

"github.com/Roverr/rtsp-stream/core/config"
jwt "github.com/dgrijalva/jwt-go"
"github.com/sirupsen/logrus"
)

// JWT interface describes how token validation looks like
type JWT interface {
Validate(token string) bool
}

// JWTProvider implements the validate method
type JWTProvider struct {
secret []byte
verifyKey *rsa.PublicKey
}

// Implementation check
var _ JWT = (*JWTProvider)(nil)

// NewJWTProvider returns a new pointer for the created provider
func NewJWTProvider(settings config.Auth) (*JWTProvider, error) {
switch strings.ToLower(settings.JWTMethod) {
case "rsa":
verifyBytes, err := ioutil.ReadFile(settings.JWTPubKeyPath)
if err != nil {
return nil, err
}
verifyKey, err := jwt.ParseRSAPublicKeyFromPEM(verifyBytes)
if err != nil {
return nil, err
}
return &JWTProvider{verifyKey: verifyKey}, nil
default:
return &JWTProvider{secret: []byte(settings.JWTSecret)}, nil
}
}

// Validate is for validating if the given token is authenticated
func (jp JWTProvider) Validate(tokenString string) bool {
ts := strings.Replace(tokenString, "Bearer ", "", -1)
token, err := jwt.Parse(ts, jp.verify)
if err != nil {
logrus.Errorln("Error at token verification ", err)
return false
}
return token.Valid
}

// verify is to check the signing method and return the secret
func (jp JWTProvider) verify(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); ok {
return jp.secret, nil
}
if _, ok := token.Method.(*jwt.SigningMethodRSA); ok {
return jp.verifyKey, nil
}
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
36 changes: 36 additions & 0 deletions core/auth/jwt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package auth

import (
"crypto/rand"
"crypto/rsa"
"fmt"
"testing"

"github.com/Roverr/rtsp-stream/core/config"
jwt "github.com/dgrijalva/jwt-go"
"github.com/stretchr/testify/assert"
)

func TestJWTAuthWithSecret(t *testing.T) {
spec := config.InitConfig()
provider, err := NewJWTProvider(spec.Auth)
assert.Nil(t, err)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{})
tokenString, err := token.SignedString([]byte(spec.Auth.JWTSecret))
assert.Nil(t, err)
assert.True(t, provider.Validate(tokenString))
assert.True(t, provider.Validate(fmt.Sprintf("Bearer %s", tokenString)))
}

func TestJWTAuthWithRSA(t *testing.T) {
reader := rand.Reader
bitSize := 2048
key, err := rsa.GenerateKey(reader, bitSize)
assert.Nil(t, err)
publicKey := key.PublicKey
provider := JWTProvider{verifyKey: &publicKey}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{})
tokenString, err := token.SignedString(key)
assert.Nil(t, err)
assert.True(t, provider.Validate(tokenString))
}
9 changes: 9 additions & 0 deletions core/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ type CORS struct {
MaxAge int `envconfig:"CORS_MAX_AGE" default:"0"` // Indicates how long (in seconds) the results of a preflight request can be cached.
}

// Auth describes information regarding authentication
type Auth struct {
JWTEnabled bool `envconfig:"AUTH_JWT_ENABLED" default:"false"` // Indicates if JWT authentication is enabled or not
JWTSecret string `envconfig:"AUTH_JWT_SECRET" default:"macilaci"` // Secret of the JWT encryption
JWTMethod string `envconfig:"AUTH_JWT_METHOD" default:"secret"` // Can be "secret" or "rsa", defines the decoding method
JWTPubKeyPath string `envconfig:"AUTH_JWT_PUB_PATH" default:"./key.pub"` // Path to the public RSA key
}

// Specification describes the application context settings
type Specification struct {
Debug bool `envconfig:"DEBUG" default:"false"` // Indicates if debug log should be enabled or not
Expand All @@ -24,6 +32,7 @@ type Specification struct {
ListEndpoint bool `envconfig:"LIST_ENDPOINT" default:"false"` // Turns on / off the stream listing endpoint feature

CORS
Auth
}

// InitConfig is to initalise the config
Expand Down
37 changes: 36 additions & 1 deletion core/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"syscall"
"time"

"github.com/Roverr/rtsp-stream/core/auth"
"github.com/Roverr/rtsp-stream/core/config"
"github.com/Roverr/rtsp-stream/core/streaming"
"github.com/julienschmidt/httprouter"
Expand Down Expand Up @@ -53,12 +54,25 @@ type Controller struct {
manager IManager
processor streaming.IProcessor
timeout time.Duration
jwt auth.JWT
}

// NewController creates a new instance of Controller
func NewController(spec *config.Specification, fileServer http.Handler) *Controller {
manager := NewManager(time.Second * 10)
return &Controller{spec, map[string]*streaming.Stream{}, fileServer, *manager, streaming.NewProcessor(spec.StoreDir), time.Second * 15}
provider, err := auth.NewJWTProvider(spec.Auth)
if err != nil {
logrus.Fatal("Could not create new JWT provider: ", err)
}
return &Controller{
spec,
map[string]*streaming.Stream{},
fileServer,
*manager,
streaming.NewProcessor(spec.StoreDir),
time.Second * 15,
provider,
}
}

// SendError sends an error to the client
Expand All @@ -69,8 +83,21 @@ func (c *Controller) SendError(w http.ResponseWriter, err error, status int) {
w.Write(b)
}

// isAuthenticated is for checking if the user's request is valid or not
// from a given authentication strategy's perspective
func (c *Controller) isAuthenticated(r *http.Request) bool {
if c.spec.JWTEnabled {
return c.jwt.Validate(r.Header.Get("Authorization"))
}
return true
}

// ListStreamHandler is the HTTP handler of the /list call
func (c *Controller) ListStreamHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
if !c.isAuthenticated(r) {
w.WriteHeader(http.StatusForbidden)
return
}
dto := []*SummariseDto{}
for key, stream := range c.streams {
dto = append(dto, &SummariseDto{URI: fmt.Sprintf("/stream/%s/index.m3u8", key), Running: stream.Streak.IsActive()})
Expand All @@ -86,6 +113,10 @@ func (c *Controller) ListStreamHandler(w http.ResponseWriter, r *http.Request, _

// StartStreamHandler is an HTTP handler for the /start endpoint
func (c *Controller) StartStreamHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
if !c.isAuthenticated(r) {
w.WriteHeader(http.StatusForbidden)
return
}
var dto StreamDto
if err := c.marshalValidatedURI(&dto, r.Body); err != nil {
logrus.Error(err)
Expand Down Expand Up @@ -198,6 +229,10 @@ func (c *Controller) cleanUnused() {

// FileHandler is HTTP handler for direct file requests
func (c *Controller) FileHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
if !c.isAuthenticated(req) {
w.WriteHeader(http.StatusForbidden)
return
}
defer c.fileServer.ServeHTTP(w, req)
filepath := ps.ByName("filepath")
req.URL.Path = filepath
Expand Down
Loading

0 comments on commit 378424a

Please sign in to comment.