mirror of
https://github.com/bluenviron/mediamtx.git
synced 2025-12-20 02:00:05 -08:00
316 lines
6.6 KiB
Go
316 lines
6.6 KiB
Go
// Package auth contains the authentication system.
|
|
package auth
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/MicahParks/keyfunc/v3"
|
|
"github.com/bluenviron/mediamtx/internal/conf"
|
|
"github.com/bluenviron/mediamtx/internal/protocols/tls"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const (
|
|
// PauseAfterError is the pause to apply after an authentication failure.
|
|
PauseAfterError = 2 * time.Second
|
|
|
|
jwksRefreshPeriod = 60 * 60 * time.Second
|
|
)
|
|
|
|
func isHTTPRequest(r *Request) bool {
|
|
switch r.Action {
|
|
case conf.AuthActionPlayback, conf.AuthActionAPI,
|
|
conf.AuthActionMetrics, conf.AuthActionPprof:
|
|
return true
|
|
}
|
|
|
|
switch r.Protocol {
|
|
case ProtocolHLS, ProtocolWebRTC:
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Error is a authentication error.
|
|
type Error struct {
|
|
Wrapped error
|
|
Message string
|
|
AskCredentials bool
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e Error) Error() string {
|
|
return "authentication failed: " + e.Wrapped.Error()
|
|
}
|
|
|
|
func matchesPermission(perms []conf.AuthInternalUserPermission, req *Request) bool {
|
|
for _, perm := range perms {
|
|
if perm.Action == req.Action {
|
|
if perm.Action == conf.AuthActionPublish ||
|
|
perm.Action == conf.AuthActionRead ||
|
|
perm.Action == conf.AuthActionPlayback {
|
|
switch {
|
|
case perm.Path == "":
|
|
return true
|
|
|
|
case strings.HasPrefix(perm.Path, "~"):
|
|
regexp, err := regexp.Compile(perm.Path[1:])
|
|
if err == nil && regexp.MatchString(req.Path) {
|
|
return true
|
|
}
|
|
|
|
case perm.Path == req.Path:
|
|
return true
|
|
}
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Manager is the authentication manager.
|
|
type Manager struct {
|
|
Method conf.AuthMethod
|
|
InternalUsers []conf.AuthInternalUser
|
|
HTTPAddress string
|
|
HTTPExclude []conf.AuthInternalUserPermission
|
|
JWTJWKS string
|
|
JWTJWKSFingerprint string
|
|
JWTClaimKey string
|
|
JWTExclude []conf.AuthInternalUserPermission
|
|
JWTInHTTPQuery bool
|
|
ReadTimeout time.Duration
|
|
|
|
mutex sync.RWMutex
|
|
jwksLastRefresh time.Time
|
|
jwtKeyFunc keyfunc.Keyfunc
|
|
}
|
|
|
|
// ReloadInternalUsers reloads InternalUsers.
|
|
func (m *Manager) ReloadInternalUsers(u []conf.AuthInternalUser) {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.InternalUsers = u
|
|
}
|
|
|
|
// Authenticate authenticates a request.
|
|
func (m *Manager) Authenticate(req *Request) error {
|
|
var err error
|
|
|
|
switch m.Method {
|
|
case conf.AuthMethodInternal:
|
|
err = m.authenticateInternal(req)
|
|
|
|
case conf.AuthMethodHTTP:
|
|
err = m.authenticateHTTP(req)
|
|
|
|
default:
|
|
err = m.authenticateJWT(req)
|
|
}
|
|
|
|
if err != nil {
|
|
return Error{
|
|
Wrapped: err,
|
|
AskCredentials: m.Method != conf.AuthMethodJWT && req.Credentials.User == "" && req.Credentials.Pass == "",
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) authenticateInternal(req *Request) error {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
for _, u := range m.InternalUsers {
|
|
if ok := m.authenticateWithUser(req, &u); ok {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("authentication failed")
|
|
}
|
|
|
|
func (m *Manager) authenticateWithUser(
|
|
req *Request,
|
|
u *conf.AuthInternalUser,
|
|
) bool {
|
|
if len(u.IPs) != 0 && !u.IPs.Contains(req.IP) {
|
|
return false
|
|
}
|
|
|
|
if !matchesPermission(u.Permissions, req) {
|
|
return false
|
|
}
|
|
|
|
if u.User != "any" {
|
|
if req.CustomVerifyFunc != nil {
|
|
if ok := req.CustomVerifyFunc(string(u.User), string(u.Pass)); !ok {
|
|
return false
|
|
}
|
|
} else {
|
|
if !u.User.Check(req.Credentials.User) || !u.Pass.Check(req.Credentials.Pass) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (m *Manager) authenticateHTTP(req *Request) error {
|
|
if matchesPermission(m.HTTPExclude, req) {
|
|
return nil
|
|
}
|
|
|
|
enc, _ := json.Marshal(struct {
|
|
IP string `json:"ip"`
|
|
User string `json:"user"`
|
|
Password string `json:"password"`
|
|
Token string `json:"token"`
|
|
Action string `json:"action"`
|
|
Path string `json:"path"`
|
|
Protocol string `json:"protocol"`
|
|
ID *uuid.UUID `json:"id"`
|
|
Query string `json:"query"`
|
|
}{
|
|
IP: req.IP.String(),
|
|
User: req.Credentials.User,
|
|
Password: req.Credentials.Pass,
|
|
Token: req.Credentials.Token,
|
|
Action: string(req.Action),
|
|
Path: req.Path,
|
|
Protocol: string(req.Protocol),
|
|
ID: req.ID,
|
|
Query: req.Query,
|
|
})
|
|
|
|
res, err := http.Post(m.HTTPAddress, "application/json", bytes.NewReader(enc))
|
|
if err != nil {
|
|
return fmt.Errorf("HTTP request failed: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode < 200 || res.StatusCode > 299 {
|
|
if resBody, err2 := io.ReadAll(res.Body); err2 == nil && len(resBody) != 0 {
|
|
return fmt.Errorf("server replied with code %d: %s", res.StatusCode, string(resBody))
|
|
}
|
|
|
|
return fmt.Errorf("server replied with code %d", res.StatusCode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) authenticateJWT(req *Request) error {
|
|
if matchesPermission(m.JWTExclude, req) {
|
|
return nil
|
|
}
|
|
|
|
keyfunc, err := m.pullJWTJWKS()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var encodedJWT string
|
|
|
|
switch {
|
|
case req.Credentials.Token != "":
|
|
encodedJWT = req.Credentials.Token
|
|
|
|
case req.Credentials.Pass != "":
|
|
encodedJWT = req.Credentials.Pass
|
|
|
|
case (!isHTTPRequest(req) || m.JWTInHTTPQuery):
|
|
var v url.Values
|
|
v, err = url.ParseQuery(req.Query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(v["jwt"]) != 1 || len(v["jwt"][0]) == 0 {
|
|
return fmt.Errorf("JWT not provided")
|
|
}
|
|
|
|
encodedJWT = v["jwt"][0]
|
|
|
|
default:
|
|
return fmt.Errorf("JWT not provided")
|
|
}
|
|
|
|
var cc jwtClaims
|
|
cc.permissionsKey = m.JWTClaimKey
|
|
_, err = jwt.ParseWithClaims(encodedJWT, &cc, keyfunc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !matchesPermission(cc.permissions, req) {
|
|
return fmt.Errorf("user doesn't have permission to perform action")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) pullJWTJWKS() (jwt.Keyfunc, error) {
|
|
now := time.Now()
|
|
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
if now.Sub(m.jwksLastRefresh) >= jwksRefreshPeriod {
|
|
tr := &http.Transport{
|
|
TLSClientConfig: tls.ConfigForFingerprint(m.JWTJWKSFingerprint),
|
|
}
|
|
defer tr.CloseIdleConnections()
|
|
|
|
httpClient := &http.Client{
|
|
Timeout: (m.ReadTimeout),
|
|
Transport: tr,
|
|
}
|
|
|
|
res, err := httpClient.Get(m.JWTJWKS)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
var raw json.RawMessage
|
|
err = json.NewDecoder(res.Body).Decode(&raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tmp, err := keyfunc.NewJWKSetJSON(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m.jwtKeyFunc = tmp
|
|
m.jwksLastRefresh = now
|
|
}
|
|
|
|
return m.jwtKeyFunc.Keyfunc, nil
|
|
}
|
|
|
|
// RefreshJWTJWKS refreshes the JWT JWKS.
|
|
func (m *Manager) RefreshJWTJWKS() {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
m.jwksLastRefresh = time.Time{}
|
|
}
|