Skip to content

Commit

Permalink
allow to set the JWT claim key that contains permissions (#3560)
Browse files Browse the repository at this point in the history
  • Loading branch information
aler9 committed Aug 26, 2024
1 parent 6da35c8 commit 3cc6f40
Show file tree
Hide file tree
Showing 8 changed files with 55 additions and 7 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1147,7 +1147,7 @@ If the URL returns a status code that begins with `20` (i.e. `200`), authenticat
```json
{
"user": "",
"password": "",
"password": ""
}
```
Expand All @@ -1171,9 +1171,10 @@ Authentication can be delegated to an external identity server, that is capable
```yml
authMethod: jwt
authJWTJWKS: http://my_identity_server/jwks_endpoint
authJWTClaimKey: mediamtx_permissions
```
The JWT is expected to contain the `mediamtx_permissions` scope, with a list of permissions in the same format as the one of user permissions:
The JWT is expected to contain a claim, with a list of permissions in the same format as the one of user permissions:
```json
{
Expand Down
2 changes: 2 additions & 0 deletions apidocs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ components:
$ref: '#/components/schemas/AuthInternalUserPermission'
authJWTJWKS:
type: string
authJWTClaimKey:
type: string

# Control API
api:
Expand Down
32 changes: 30 additions & 2 deletions internal/auth/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,33 @@ func matchesPermission(perms []conf.AuthInternalUserPermission, req *Request) bo

type customClaims struct {
jwt.RegisteredClaims
MediaMTXPermissions []conf.AuthInternalUserPermission `json:"mediamtx_permissions"`
permissionsKey string
permissions []conf.AuthInternalUserPermission
}

func (c *customClaims) UnmarshalJSON(b []byte) error {
err := json.Unmarshal(b, &c.RegisteredClaims)
if err != nil {
return err
}

Check warning on line 110 in internal/auth/manager.go

View check run for this annotation

Codecov / codecov/patch

internal/auth/manager.go#L109-L110

Added lines #L109 - L110 were not covered by tests

var claimMap map[string]json.RawMessage
err = json.Unmarshal(b, &claimMap)
if err != nil {
return err
}

Check warning on line 116 in internal/auth/manager.go

View check run for this annotation

Codecov / codecov/patch

internal/auth/manager.go#L115-L116

Added lines #L115 - L116 were not covered by tests

rawPermissions, ok := claimMap[c.permissionsKey]
if !ok {
return fmt.Errorf("claim '%s' not found inside JWT", c.permissionsKey)
}

Check warning on line 121 in internal/auth/manager.go

View check run for this annotation

Codecov / codecov/patch

internal/auth/manager.go#L120-L121

Added lines #L120 - L121 were not covered by tests

err = json.Unmarshal(rawPermissions, &c.permissions)
if err != nil {
return err
}

Check warning on line 126 in internal/auth/manager.go

View check run for this annotation

Codecov / codecov/patch

internal/auth/manager.go#L125-L126

Added lines #L125 - L126 were not covered by tests

return nil
}

// Manager is the authentication manager.
Expand All @@ -109,6 +135,7 @@ type Manager struct {
HTTPAddress string
HTTPExclude []conf.AuthInternalUserPermission
JWTJWKS string
JWTClaimKey string
ReadTimeout time.Duration
RTSPAuthMethods []auth.ValidateMethod

Expand Down Expand Up @@ -270,12 +297,13 @@ func (m *Manager) authenticateJWT(req *Request) error {
}

var cc customClaims
cc.permissionsKey = m.JWTClaimKey
_, err = jwt.ParseWithClaims(v["jwt"][0], &cc, keyfunc)
if err != nil {
return err
}

if !matchesPermission(cc.MediaMTXPermissions, req) {
if !matchesPermission(cc.permissions, req) {
return fmt.Errorf("user doesn't have permission to perform action")
}

Expand Down
7 changes: 4 additions & 3 deletions internal/auth/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ func TestAuthJWT(t *testing.T) {

type customClaims struct {
jwt.RegisteredClaims
MediaMTXPermissions []conf.AuthInternalUserPermission `json:"mediamtx_permissions"`
MediaMTXPermissions []conf.AuthInternalUserPermission `json:"my_permission_key"`
}

claims := customClaims{
Expand All @@ -351,8 +351,9 @@ func TestAuthJWT(t *testing.T) {
require.NoError(t, err)

m := Manager{
Method: conf.AuthMethodJWT,
JWTJWKS: "http://localhost:4567/jwks",
Method: conf.AuthMethodJWT,
JWTJWKS: "http://localhost:4567/jwks",
JWTClaimKey: "my_permission_key",
}

err = m.Authenticate(&Request{
Expand Down
5 changes: 5 additions & 0 deletions internal/conf/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ type Conf struct {
ExternalAuthenticationURL *string `json:"externalAuthenticationURL,omitempty"` // deprecated
AuthHTTPExclude AuthInternalUserPermissions `json:"authHTTPExclude"`
AuthJWTJWKS string `json:"authJWTJWKS"`
AuthJWTClaimKey string `json:"authJWTClaimKey"`

// Control API
API bool `json:"api"`
Expand Down Expand Up @@ -323,6 +324,7 @@ func (conf *Conf) setDefaults() {
Action: AuthActionPprof,
},
}
conf.AuthJWTClaimKey = "mediamtx_permissions"

// Control API
conf.APIAddress = ":9997"
Expand Down Expand Up @@ -562,6 +564,9 @@ func (conf *Conf) Validate() error {
if conf.AuthJWTJWKS == "" {
return fmt.Errorf("'authJWTJWKS' is empty")
}
if conf.AuthJWTClaimKey == "" {
return fmt.Errorf("'authJWTClaimKey' is empty")
}
}

// RTSP
Expand Down
7 changes: 7 additions & 0 deletions internal/conf/conf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,13 @@ func TestConfErrors(t *testing.T) {
`record path './recordings/%path/%Y-%m-%d_%H-%M-%S' is missing one of the` +
` mandatory elements for the playback server to work: %Y %m %d %H %M %S %f`,
},
{
"jwt claim key empty",
"authMethod: jwt\n" +
"authJWTJWKS: https://not-real.com\n" +
"authJWTClaimKey: \"\"",
"'authJWTClaimKey' is empty",
},
} {
t.Run(ca.name, func(t *testing.T) {
tmpf, err := createTempFile([]byte(ca.conf))
Expand Down
2 changes: 2 additions & 0 deletions internal/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ func (p *Core) createResources(initial bool) error {
HTTPAddress: p.conf.AuthHTTPAddress,
HTTPExclude: p.conf.AuthHTTPExclude,
JWTJWKS: p.conf.AuthJWTJWKS,
JWTClaimKey: p.conf.AuthJWTClaimKey,
ReadTimeout: time.Duration(p.conf.ReadTimeout),
RTSPAuthMethods: p.conf.RTSPAuthMethods,
}
Expand Down Expand Up @@ -674,6 +675,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.AuthHTTPAddress != p.conf.AuthHTTPAddress ||
!reflect.DeepEqual(newConf.AuthHTTPExclude, p.conf.AuthHTTPExclude) ||
newConf.AuthJWTJWKS != p.conf.AuthJWTJWKS ||
newConf.AuthJWTClaimKey != p.conf.AuthJWTClaimKey ||
newConf.ReadTimeout != p.conf.ReadTimeout ||
!reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods)
if !closeAuthManager && !reflect.DeepEqual(newConf.AuthInternalUsers, p.conf.AuthInternalUsers) {
Expand Down
2 changes: 2 additions & 0 deletions mediamtx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ authHTTPExclude:
# This is the JWKS URL that will be used to pull (once) the public key that allows
# to validate JWTs.
authJWTJWKS:
# name of the claim that contains permissions.
authJWTClaimKey: mediamtx_permissions

###############################################
# Global settings -> Control API
Expand Down

0 comments on commit 3cc6f40

Please sign in to comment.