mirror of
https://github.com/bluenviron/mediamtx
synced 2025-02-17 03:56:55 +00:00
This commit is contained in:
parent
6da35c8041
commit
0d1da6bd5b
@ -1147,7 +1147,7 @@ If the URL returns a status code that begins with `20` (i.e. `200`), authenticat
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"user": "",
|
"user": "",
|
||||||
"password": "",
|
"password": ""
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -1171,9 +1171,10 @@ Authentication can be delegated to an external identity server, that is capable
|
|||||||
```yml
|
```yml
|
||||||
authMethod: jwt
|
authMethod: jwt
|
||||||
authJWTJWKS: http://my_identity_server/jwks_endpoint
|
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
|
```json
|
||||||
{
|
{
|
||||||
|
@ -87,6 +87,8 @@ components:
|
|||||||
$ref: '#/components/schemas/AuthInternalUserPermission'
|
$ref: '#/components/schemas/AuthInternalUserPermission'
|
||||||
authJWTJWKS:
|
authJWTJWKS:
|
||||||
type: string
|
type: string
|
||||||
|
authJWTClaimKey:
|
||||||
|
type: string
|
||||||
|
|
||||||
# Control API
|
# Control API
|
||||||
api:
|
api:
|
||||||
|
@ -99,7 +99,33 @@ func matchesPermission(perms []conf.AuthInternalUserPermission, req *Request) bo
|
|||||||
|
|
||||||
type customClaims struct {
|
type customClaims struct {
|
||||||
jwt.RegisteredClaims
|
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.
|
// Manager is the authentication manager.
|
||||||
@ -109,6 +135,7 @@ type Manager struct {
|
|||||||
HTTPAddress string
|
HTTPAddress string
|
||||||
HTTPExclude []conf.AuthInternalUserPermission
|
HTTPExclude []conf.AuthInternalUserPermission
|
||||||
JWTJWKS string
|
JWTJWKS string
|
||||||
|
JWTClaimKey string
|
||||||
ReadTimeout time.Duration
|
ReadTimeout time.Duration
|
||||||
RTSPAuthMethods []auth.ValidateMethod
|
RTSPAuthMethods []auth.ValidateMethod
|
||||||
|
|
||||||
@ -270,12 +297,13 @@ func (m *Manager) authenticateJWT(req *Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var cc customClaims
|
var cc customClaims
|
||||||
|
cc.permissionsKey = m.JWTClaimKey
|
||||||
_, err = jwt.ParseWithClaims(v["jwt"][0], &cc, keyfunc)
|
_, err = jwt.ParseWithClaims(v["jwt"][0], &cc, keyfunc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !matchesPermission(cc.MediaMTXPermissions, req) {
|
if !matchesPermission(cc.permissions, req) {
|
||||||
return fmt.Errorf("user doesn't have permission to perform action")
|
return fmt.Errorf("user doesn't have permission to perform action")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,7 +327,7 @@ func TestAuthJWT(t *testing.T) {
|
|||||||
|
|
||||||
type customClaims struct {
|
type customClaims struct {
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
MediaMTXPermissions []conf.AuthInternalUserPermission `json:"mediamtx_permissions"`
|
MediaMTXPermissions []conf.AuthInternalUserPermission `json:"my_permission_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
claims := customClaims{
|
claims := customClaims{
|
||||||
@ -351,8 +351,9 @@ func TestAuthJWT(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
m := Manager{
|
m := Manager{
|
||||||
Method: conf.AuthMethodJWT,
|
Method: conf.AuthMethodJWT,
|
||||||
JWTJWKS: "http://localhost:4567/jwks",
|
JWTJWKS: "http://localhost:4567/jwks",
|
||||||
|
JWTClaimKey: "my_permission_key",
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.Authenticate(&Request{
|
err = m.Authenticate(&Request{
|
||||||
|
@ -177,6 +177,7 @@ type Conf struct {
|
|||||||
ExternalAuthenticationURL *string `json:"externalAuthenticationURL,omitempty"` // deprecated
|
ExternalAuthenticationURL *string `json:"externalAuthenticationURL,omitempty"` // deprecated
|
||||||
AuthHTTPExclude AuthInternalUserPermissions `json:"authHTTPExclude"`
|
AuthHTTPExclude AuthInternalUserPermissions `json:"authHTTPExclude"`
|
||||||
AuthJWTJWKS string `json:"authJWTJWKS"`
|
AuthJWTJWKS string `json:"authJWTJWKS"`
|
||||||
|
AuthJWTClaimKey string `json:"authJWTClaimKey"`
|
||||||
|
|
||||||
// Control API
|
// Control API
|
||||||
API bool `json:"api"`
|
API bool `json:"api"`
|
||||||
@ -323,6 +324,7 @@ func (conf *Conf) setDefaults() {
|
|||||||
Action: AuthActionPprof,
|
Action: AuthActionPprof,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
conf.AuthJWTClaimKey = "mediamtx_permissions"
|
||||||
|
|
||||||
// Control API
|
// Control API
|
||||||
conf.APIAddress = ":9997"
|
conf.APIAddress = ":9997"
|
||||||
@ -562,6 +564,9 @@ func (conf *Conf) Validate() error {
|
|||||||
if conf.AuthJWTJWKS == "" {
|
if conf.AuthJWTJWKS == "" {
|
||||||
return fmt.Errorf("'authJWTJWKS' is empty")
|
return fmt.Errorf("'authJWTJWKS' is empty")
|
||||||
}
|
}
|
||||||
|
if conf.AuthJWTClaimKey == "" {
|
||||||
|
return fmt.Errorf("'authJWTClaimKey' is empty")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RTSP
|
// RTSP
|
||||||
|
@ -357,6 +357,13 @@ func TestConfErrors(t *testing.T) {
|
|||||||
`record path './recordings/%path/%Y-%m-%d_%H-%M-%S' is missing one of the` +
|
`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`,
|
` 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) {
|
t.Run(ca.name, func(t *testing.T) {
|
||||||
tmpf, err := createTempFile([]byte(ca.conf))
|
tmpf, err := createTempFile([]byte(ca.conf))
|
||||||
|
@ -287,6 +287,7 @@ func (p *Core) createResources(initial bool) error {
|
|||||||
HTTPAddress: p.conf.AuthHTTPAddress,
|
HTTPAddress: p.conf.AuthHTTPAddress,
|
||||||
HTTPExclude: p.conf.AuthHTTPExclude,
|
HTTPExclude: p.conf.AuthHTTPExclude,
|
||||||
JWTJWKS: p.conf.AuthJWTJWKS,
|
JWTJWKS: p.conf.AuthJWTJWKS,
|
||||||
|
JWTClaimKey: p.conf.AuthJWTClaimKey,
|
||||||
ReadTimeout: time.Duration(p.conf.ReadTimeout),
|
ReadTimeout: time.Duration(p.conf.ReadTimeout),
|
||||||
RTSPAuthMethods: p.conf.RTSPAuthMethods,
|
RTSPAuthMethods: p.conf.RTSPAuthMethods,
|
||||||
}
|
}
|
||||||
@ -674,6 +675,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
|||||||
newConf.AuthHTTPAddress != p.conf.AuthHTTPAddress ||
|
newConf.AuthHTTPAddress != p.conf.AuthHTTPAddress ||
|
||||||
!reflect.DeepEqual(newConf.AuthHTTPExclude, p.conf.AuthHTTPExclude) ||
|
!reflect.DeepEqual(newConf.AuthHTTPExclude, p.conf.AuthHTTPExclude) ||
|
||||||
newConf.AuthJWTJWKS != p.conf.AuthJWTJWKS ||
|
newConf.AuthJWTJWKS != p.conf.AuthJWTJWKS ||
|
||||||
|
newConf.AuthJWTClaimKey != p.conf.AuthJWTClaimKey ||
|
||||||
newConf.ReadTimeout != p.conf.ReadTimeout ||
|
newConf.ReadTimeout != p.conf.ReadTimeout ||
|
||||||
!reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods)
|
!reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods)
|
||||||
if !closeAuthManager && !reflect.DeepEqual(newConf.AuthInternalUsers, p.conf.AuthInternalUsers) {
|
if !closeAuthManager && !reflect.DeepEqual(newConf.AuthInternalUsers, p.conf.AuthInternalUsers) {
|
||||||
|
@ -121,6 +121,8 @@ authHTTPExclude:
|
|||||||
# This is the JWKS URL that will be used to pull (once) the public key that allows
|
# This is the JWKS URL that will be used to pull (once) the public key that allows
|
||||||
# to validate JWTs.
|
# to validate JWTs.
|
||||||
authJWTJWKS:
|
authJWTJWKS:
|
||||||
|
# name of the claim that contains permissions.
|
||||||
|
authJWTClaimKey: mediamtx_permissions
|
||||||
|
|
||||||
###############################################
|
###############################################
|
||||||
# Global settings -> Control API
|
# Global settings -> Control API
|
||||||
|
Loading…
Reference in New Issue
Block a user