allow to set the JWT claim key that contains permissions (#3560) (#3692)

This commit is contained in:
Alessandro Ros 2024-08-26 12:43:28 +02:00 committed by GitHub
parent 6da35c8041
commit 0d1da6bd5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 55 additions and 7 deletions

View File

@ -1147,7 +1147,7 @@ If the URL returns a status code that begins with `20` (i.e. `200`), authenticat
```json
{
"user": "",
"password": "",
"password": ""
}
```
@ -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
{

View File

@ -87,6 +87,8 @@ components:
$ref: '#/components/schemas/AuthInternalUserPermission'
authJWTJWKS:
type: string
authJWTClaimKey:
type: string
# Control API
api:

View File

@ -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
}
var claimMap map[string]json.RawMessage
err = json.Unmarshal(b, &claimMap)
if err != nil {
return err
}
rawPermissions, ok := claimMap[c.permissionsKey]
if !ok {
return fmt.Errorf("claim '%s' not found inside JWT", c.permissionsKey)
}
err = json.Unmarshal(rawPermissions, &c.permissions)
if err != nil {
return err
}
return nil
}
// Manager is the authentication manager.
@ -109,6 +135,7 @@ type Manager struct {
HTTPAddress string
HTTPExclude []conf.AuthInternalUserPermission
JWTJWKS string
JWTClaimKey string
ReadTimeout time.Duration
RTSPAuthMethods []auth.ValidateMethod
@ -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")
}

View File

@ -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{
@ -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{

View File

@ -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"`
@ -323,6 +324,7 @@ func (conf *Conf) setDefaults() {
Action: AuthActionPprof,
},
}
conf.AuthJWTClaimKey = "mediamtx_permissions"
// Control API
conf.APIAddress = ":9997"
@ -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

View File

@ -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))

View File

@ -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,
}
@ -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) {

View File

@ -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