diff --git a/api/openapi.yaml b/api/openapi.yaml index 90cb47d97..d8832885e 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -289,6 +289,10 @@ components: $ref: "#/components/schemas/AuthInternalUserPermission" authHTTPFingerprint: type: string + authHTTPForceInternalUsersForActions: + type: array + items: + $ref: "#/components/schemas/AuthInternalUserPermission" authInternalUsers: type: array items: diff --git a/docs/2-features/06-authentication.md b/docs/2-features/06-authentication.md index 98f5e3706..888cab5e7 100644 --- a/docs/2-features/06-authentication.md +++ b/docs/2-features/06-authentication.md @@ -121,6 +121,27 @@ authHTTPExclude: - action: pprof ``` +Some of the excluded actions can be validated through internal users instead of being left unauthenticated: + +```yml +authHTTPExclude: + - action: publish + - action: api + - action: metrics + - action: pprof +authHTTPForceInternalUsersForActions: + - action: publish + - action: api +authInternalUsers: + - user: internal + pass: internalpass + permissions: + - action: publish + - action: api +``` + +In this example, `read` is still validated through the external HTTP server, while `publish` and `api` are validated through internal users. `metrics` and `pprof` remain excluded from any authentication. + If the authentication server uses HTTPS and has a self-signed or invalid TLS certificate, you can provide the fingerprint of the certificate to validate it anyway: ```yml diff --git a/internal/auth/manager.go b/internal/auth/manager.go index 263b4c8e9..3fce992e2 100644 --- a/internal/auth/manager.go +++ b/internal/auth/manager.go @@ -90,19 +90,20 @@ func getToken(tokenInHTTPQuery bool, req *Request) string { // Manager is the authentication manager. type Manager struct { - Method conf.AuthMethod - InternalUsers []conf.AuthInternalUser - HTTPAddress string - HTTPFingerprint string - HTTPExclude []conf.AuthInternalUserPermission - JWTJWKS string - JWTJWKSFingerprint string - JWTClaimKey string - JWTExclude []conf.AuthInternalUserPermission - JWTInHTTPQuery *bool - JWTIssuer string - JWTAudience string - ReadTimeout time.Duration + Method conf.AuthMethod + InternalUsers []conf.AuthInternalUser + HTTPAddress string + HTTPFingerprint string + HTTPExclude []conf.AuthInternalUserPermission + HTTPForceInternalUsersForActions []conf.AuthInternalUserPermission + JWTJWKS string + JWTJWKSFingerprint string + JWTClaimKey string + JWTExclude []conf.AuthInternalUserPermission + JWTInHTTPQuery *bool + JWTIssuer string + JWTAudience string + ReadTimeout time.Duration mutex sync.RWMutex jwksLastRefresh time.Time @@ -190,6 +191,9 @@ func (m *Manager) authenticateWithUser( func (m *Manager) authenticateHTTP(req *Request, token string) (string, error) { if matchesPermission(m.HTTPExclude, req) { + if matchesPermission(m.HTTPForceInternalUsersForActions, req) { + return m.authenticateInternal(req) + } return "", nil } diff --git a/internal/auth/manager_test.go b/internal/auth/manager_test.go index ef65fa762..fca5b9c05 100644 --- a/internal/auth/manager_test.go +++ b/internal/auth/manager_test.go @@ -455,6 +455,117 @@ func TestAuthHTTPExclude(t *testing.T) { require.Equal(t, "", user) } +func TestAuthHTTPForceInternalUsersForActions(t *testing.T) { + m := Manager{ + Method: conf.AuthMethodHTTP, + HTTPAddress: "http://not-to-be-used:9120/auth", + HTTPExclude: []conf.AuthInternalUserPermission{{ + Action: conf.AuthActionPublish, + }, { + Action: conf.AuthActionAPI, + }}, + HTTPForceInternalUsersForActions: []conf.AuthInternalUserPermission{{ + Action: conf.AuthActionPublish, + }}, + InternalUsers: []conf.AuthInternalUser{ + { + User: conf.Credential("pubuser"), + Pass: conf.Credential("pubpass"), + Permissions: []conf.AuthInternalUserPermission{{ + Action: conf.AuthActionPublish, + }}, + }, + { + User: conf.Credential("apiuser"), + Pass: conf.Credential("apipass"), + Permissions: []conf.AuthInternalUserPermission{{ + Action: conf.AuthActionAPI, + }}, + }, + }, + } + + t.Run("publish with valid credentials", func(t *testing.T) { + user, err := m.Authenticate(&Request{ + Action: conf.AuthActionPublish, + Path: "teststream", + Protocol: ProtocolRTSP, + Credentials: &Credentials{ + User: "pubuser", + Pass: "pubpass", + }, + IP: net.ParseIP("127.0.0.1"), + }) + require.Nil(t, err) + require.Equal(t, "pubuser", user) + }) + + t.Run("publish with invalid credentials", func(t *testing.T) { + _, err := m.Authenticate(&Request{ + Action: conf.AuthActionPublish, + Path: "teststream", + Protocol: ProtocolRTSP, + Credentials: &Credentials{ + User: "wrong", + Pass: "wrong", + }, + IP: net.ParseIP("127.0.0.1"), + }) + require.NotNil(t, err) + }) + + t.Run("api excluded but not forced to internal", func(t *testing.T) { + user, err := m.Authenticate(&Request{ + Action: conf.AuthActionAPI, + Path: "", + Protocol: ProtocolRTSP, + Credentials: &Credentials{ + User: "", + Pass: "", + }, + IP: net.ParseIP("127.0.0.1"), + }) + require.Nil(t, err) + require.Equal(t, "", user) + }) + + t.Run("second matching user", func(t *testing.T) { + m2 := m + m2.HTTPForceInternalUsersForActions = []conf.AuthInternalUserPermission{{ + Action: conf.AuthActionPublish, + }} + m2.InternalUsers = []conf.AuthInternalUser{ + { + User: conf.Credential("wronguser"), + Pass: conf.Credential("wrongpass"), + Permissions: []conf.AuthInternalUserPermission{{ + Action: conf.AuthActionPublish, + }}, + }, + { + User: conf.Credential("pubuser"), + Pass: conf.Credential("pubpass"), + Permissions: []conf.AuthInternalUserPermission{{ + Action: conf.AuthActionPublish, + }}, + }, + } + + user, err := m2.Authenticate(&Request{ + Action: conf.AuthActionPublish, + Path: "teststream", + Protocol: ProtocolRTSP, + Credentials: &Credentials{ + User: "pubuser", + Pass: "pubpass", + }, + IP: net.ParseIP("127.0.0.1"), + }) + require.Nil(t, err) + require.Equal(t, "pubuser", user) + }) +} + func TestAuthJWT(t *testing.T) { for _, ca := range []string{"object", "string"} { t.Run(ca, func(t *testing.T) { diff --git a/internal/conf/conf.go b/internal/conf/conf.go index c8b91874b..72baa6874 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -262,7 +262,8 @@ type Conf struct { AuthHTTPAddress string `json:"authHTTPAddress"` ExternalAuthenticationURL *string `json:"externalAuthenticationURL,omitempty" deprecated:"true"` AuthHTTPFingerprint string `json:"authHTTPFingerprint"` - AuthHTTPExclude []AuthInternalUserPermission `json:"authHTTPExclude"` + AuthHTTPExclude []AuthInternalUserPermission `json:"authHTTPExclude"` + AuthHTTPForceInternalUsersForActions []AuthInternalUserPermission `json:"authHTTPForceInternalUsersForActions"` AuthJWTJWKS string `json:"authJWTJWKS"` AuthJWTJWKSFingerprint string `json:"authJWTJWKSFingerprint"` AuthJWTClaimKey string `json:"authJWTClaimKey"` diff --git a/internal/core/core.go b/internal/core/core.go index 2f935c184..00a3382bf 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -355,7 +355,8 @@ func (p *Core) createResources(initial bool) error { InternalUsers: p.conf.AuthInternalUsers, HTTPAddress: p.conf.AuthHTTPAddress, HTTPFingerprint: p.conf.AuthHTTPFingerprint, - HTTPExclude: p.conf.AuthHTTPExclude, + HTTPExclude: p.conf.AuthHTTPExclude, + HTTPForceInternalUsersForActions: p.conf.AuthHTTPForceInternalUsersForActions, JWTJWKS: p.conf.AuthJWTJWKS, JWTJWKSFingerprint: p.conf.AuthJWTJWKSFingerprint, JWTClaimKey: p.conf.AuthJWTClaimKey, @@ -782,6 +783,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { newConf.AuthHTTPAddress != p.conf.AuthHTTPAddress || newConf.AuthHTTPFingerprint != p.conf.AuthHTTPFingerprint || !reflect.DeepEqual(newConf.AuthHTTPExclude, p.conf.AuthHTTPExclude) || + !reflect.DeepEqual(newConf.AuthHTTPForceInternalUsersForActions, p.conf.AuthHTTPForceInternalUsersForActions) || newConf.AuthJWTJWKS != p.conf.AuthJWTJWKS || newConf.AuthJWTJWKSFingerprint != p.conf.AuthJWTJWKSFingerprint || newConf.AuthJWTClaimKey != p.conf.AuthJWTClaimKey || diff --git a/mediamtx.yml b/mediamtx.yml index ad5934f39..c490e7f54 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -110,6 +110,10 @@ authHTTPExclude: - action: api - action: metrics - action: pprof +# Actions excluded from HTTP-based authentication that must be validated through +# authInternalUsers instead. Format is the same as the one of user permissions. +# An action must be present in both authHTTPExclude and this parameter. +authHTTPForceInternalUsersForActions: [] # JWT-based authentication. # Users have to log in through an external identity server and obtain a JWT.