Repository: bluenviron/gortsplib
Pull request #558 (2024-05-03 21:38:51)

fix authentication when algorithm field is not supported

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/bluenviron/gortsplib/pull/558?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 81.47%. Comparing base [(`3f62e11`)](https://app.codecov.io/gh/bluenviron/gortsplib/commit/3f62e11795f69b10099ac2fee67db852e44a3c0f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) to head [(`95dd3bd`)](https://app.codecov.io/gh/bluenviron/gortsplib/pull/558?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron).

> :exclamation: Current head 95dd3bd differs from pull request most recent head d893674. Consider uploading reports for the commit d893674 to get more accurate results


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #558      +/-   ##
==========================================
- Coverage   81.49%   81.47%   -0.02%     
==========================================
  Files         115      115              
  Lines       11473    11474       +1     
==========================================
- Hits         9350     9349       -1     
- Misses       1721     1723       +2     
  Partials      402      402              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/bluenviron/gortsplib/pull/558?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron).


Patch:
diff --git a/pkg/auth/sender.go b/pkg/auth/sender.go
index 0fd14c88..de88227a 100644
--- a/pkg/auth/sender.go
+++ b/pkg/auth/sender.go
@@ -7,63 +7,41 @@ import (
 	"github.com/bluenviron/gortsplib/v4/pkg/headers"
 )
 
-func findAuthenticateHeader(auths []headers.Authenticate, method headers.AuthMethod) *headers.Authenticate {
-	for _, auth := range auths {
-		if auth.Method == method {
-			return &auth
-		}
-	}
-	return nil
-}
-
-func pickAuthenticateHeader(auths []headers.Authenticate) (*headers.Authenticate, error) {
-	if auth := findAuthenticateHeader(auths, headers.AuthDigestSHA256); auth != nil {
-		return auth, nil
-	}
-
-	if auth := findAuthenticateHeader(auths, headers.AuthDigestMD5); auth != nil {
-		return auth, nil
-	}
-
-	if auth := findAuthenticateHeader(auths, headers.AuthBasic); auth != nil {
-		return auth, nil
-	}
-
-	return nil, fmt.Errorf("no authentication methods available")
-}
-
 // Sender allows to send credentials.
 type Sender struct {
-	user               string
-	pass               string
-	authenticateHeader *headers.Authenticate
+	user       string
+	pass       string
+	authHeader *headers.Authenticate
 }
 
 // NewSender allocates a Sender.
 // It requires a WWW-Authenticate header (provided by the server)
 // and a set of credentials.
-func NewSender(vals base.HeaderValue, user string, pass string) (*Sender, error) {
-	var auths []headers.Authenticate //nolint:prealloc
+func NewSender(wwwAuth base.HeaderValue, user string, pass string) (*Sender, error) {
+	var bestAuthHeader *headers.Authenticate
 
-	for _, v := range vals {
+	for _, v := range wwwAuth {
 		var auth headers.Authenticate
 		err := auth.Unmarshal(base.HeaderValue{v})
 		if err != nil {
 			continue // ignore unrecognized headers
 		}
 
-		auths = append(auths, auth)
+		if bestAuthHeader == nil ||
+			(auth.Algorithm != nil && *auth.Algorithm == headers.AuthAlgorithmSHA256) ||
+			(bestAuthHeader.Method == headers.AuthMethodBasic) {
+			bestAuthHeader = &auth
+		}
 	}
 
-	auth, err := pickAuthenticateHeader(auths)
-	if err != nil {
-		return nil, err
+	if bestAuthHeader == nil {
+		return nil, fmt.Errorf("no authentication methods available")
 	}
 
 	return &Sender{
-		user:               user,
-		pass:               pass,
-		authenticateHeader: auth,
+		user:       user,
+		pass:       pass,
+		authHeader: bestAuthHeader,
 	}, nil
 }
 
@@ -72,29 +50,26 @@ func (se *Sender) AddAuthorization(req *base.Request) {
 	urStr := req.URL.CloneWithoutCredentials().String()
 
 	h := headers.Authorization{
-		Method: se.authenticateHeader.Method,
+		Method: se.authHeader.Method,
 	}
 
-	switch se.authenticateHeader.Method {
-	case headers.AuthBasic:
+	if se.authHeader.Method == headers.AuthMethodBasic {
 		h.BasicUser = se.user
 		h.BasicPass = se.pass
-
-	case headers.AuthDigestMD5:
+	} else { // digest
 		h.Username = se.user
-		h.Realm = se.authenticateHeader.Realm
-		h.Nonce = se.authenticateHeader.Nonce
+		h.Realm = se.authHeader.Realm
+		h.Nonce = se.authHeader.Nonce
 		h.URI = urStr
-		h.Response = md5Hex(md5Hex(se.user+":"+se.authenticateHeader.Realm+":"+se.pass) + ":" +
-			se.authenticateHeader.Nonce + ":" + md5Hex(string(req.Method)+":"+urStr))
-
-	default: // digest SHA-256
-		h.Username = se.user
-		h.Realm = se.authenticateHeader.Realm
-		h.Nonce = se.authenticateHeader.Nonce
-		h.URI = urStr
-		h.Response = sha256Hex(sha256Hex(se.user+":"+se.authenticateHeader.Realm+":"+se.pass) + ":" +
-			se.authenticateHeader.Nonce + ":" + sha256Hex(string(req.Method)+":"+urStr))
+		h.Algorithm = se.authHeader.Algorithm
+
+		if se.authHeader.Algorithm == nil || *se.authHeader.Algorithm == headers.AuthAlgorithmMD5 {
+			h.Response = md5Hex(md5Hex(se.user+":"+se.authHeader.Realm+":"+se.pass) + ":" +
+				se.authHeader.Nonce + ":" + md5Hex(string(req.Method)+":"+urStr))
+		} else { // sha256
+			h.Response = sha256Hex(sha256Hex(se.user+":"+se.authHeader.Realm+":"+se.pass) + ":" +
+				se.authHeader.Nonce + ":" + sha256Hex(string(req.Method)+":"+urStr))
+		}
 	}
 
 	if req.Header == nil {
diff --git a/pkg/auth/validate.go b/pkg/auth/validate.go
index dca2bf81..86691292 100644
--- a/pkg/auth/validate.go
+++ b/pkg/auth/validate.go
@@ -25,7 +25,7 @@ func sha256Hex(in string) string {
 	return hex.EncodeToString(h.Sum(nil))
 }
 
-func contains(list []headers.AuthMethod, item headers.AuthMethod) bool {
+func contains(list []ValidateMethod, item ValidateMethod) bool {
 	for _, i := range list {
 		if i == item {
 			return true
@@ -51,17 +51,27 @@ func urlMatches(expected string, received string, isSetup bool) bool {
 	return false
 }
 
+// ValidateMethod is a validation method.
+type ValidateMethod int
+
+// validation methods.
+const (
+	ValidateMethodBasic ValidateMethod = iota
+	ValidateMethodDigestMD5
+	ValidateMethodSHA256
+)
+
 // Validate validates a request sent by a client.
 func Validate(
 	req *base.Request,
 	user string,
 	pass string,
-	methods []headers.AuthMethod,
+	methods []ValidateMethod,
 	realm string,
 	nonce string,
 ) error {
 	if methods == nil {
-		methods = []headers.AuthMethod{headers.AuthDigestSHA256, headers.AuthDigestMD5, headers.AuthBasic}
+		methods = []ValidateMethod{ValidateMethodBasic, ValidateMethodDigestMD5, ValidateMethodSHA256}
 	}
 
 	var auth headers.Authorization
@@ -71,8 +81,11 @@ func Validate(
 	}
 
 	switch {
-	case (auth.Method == headers.AuthDigestSHA256 && contains(methods, headers.AuthDigestSHA256)) ||
-		(auth.Method == headers.AuthDigestMD5 && contains(methods, headers.AuthDigestMD5)):
+	case auth.Method == headers.AuthMethodDigest &&
+		(contains(methods, ValidateMethodDigestMD5) &&
+			(auth.Algorithm == nil || *auth.Algorithm == headers.AuthAlgorithmMD5) ||
+			contains(methods, ValidateMethodSHA256) &&
+				auth.Algorithm != nil && *auth.Algorithm == headers.AuthAlgorithmSHA256):
 		if auth.Nonce != nonce {
 			return fmt.Errorf("wrong nonce")
 		}
@@ -91,19 +104,19 @@ func Validate(
 
 		var response string
 
-		if auth.Method == headers.AuthDigestSHA256 {
-			response = sha256Hex(sha256Hex(user+":"+realm+":"+pass) +
-				":" + nonce + ":" + sha256Hex(string(req.Method)+":"+auth.URI))
-		} else {
+		if auth.Algorithm == nil || *auth.Algorithm == headers.AuthAlgorithmMD5 {
 			response = md5Hex(md5Hex(user+":"+realm+":"+pass) +
 				":" + nonce + ":" + md5Hex(string(req.Method)+":"+auth.URI))
+		} else { // sha256
+			response = sha256Hex(sha256Hex(user+":"+realm+":"+pass) +
+				":" + nonce + ":" + sha256Hex(string(req.Method)+":"+auth.URI))
 		}
 
 		if auth.Response != response {
 			return fmt.Errorf("authentication failed")
 		}
 
-	case auth.Method == headers.AuthBasic && contains(methods, headers.AuthBasic):
+	case auth.Method == headers.AuthMethodBasic && contains(methods, ValidateMethodBasic):
 		if auth.BasicUser != user {
 			return fmt.Errorf("authentication failed")
 		}
diff --git a/pkg/auth/www_authenticate.go b/pkg/auth/www_authenticate.go
index 5ee0a904..06e25f0e 100644
--- a/pkg/auth/www_authenticate.go
+++ b/pkg/auth/www_authenticate.go
@@ -6,18 +6,44 @@ import (
 )
 
 // GenerateWWWAuthenticate generates a WWW-Authenticate header.
-func GenerateWWWAuthenticate(methods []headers.AuthMethod, realm string, nonce string) base.HeaderValue {
+func GenerateWWWAuthenticate(methods []ValidateMethod, realm string, nonce string) base.HeaderValue {
 	if methods == nil {
-		methods = []headers.AuthMethod{headers.AuthDigestSHA256, headers.AuthDigestMD5, headers.AuthBasic}
+		methods = []ValidateMethod{ValidateMethodBasic, ValidateMethodDigestMD5, ValidateMethodSHA256}
 	}
 
 	var ret base.HeaderValue
+
 	for _, m := range methods {
-		ret = append(ret, headers.Authenticate{
-			Method: m,
-			Realm:  realm,
-			Nonce:  nonce, // used only by digest
-		}.Marshal()...)
+		var a base.HeaderValue
+
+		switch m {
+		case ValidateMethodBasic:
+			a = headers.Authenticate{
+				Method: headers.AuthMethodBasic,
+				Realm:  realm,
+			}.Marshal()
+
+		case ValidateMethodDigestMD5:
+			aa := headers.AuthAlgorithmMD5
+			a = headers.Authenticate{
+				Method:    headers.AuthMethodDigest,
+				Realm:     realm,
+				Nonce:     nonce,
+				Algorithm: &aa,
+			}.Marshal()
+
+		default: // sha256
+			aa := headers.AuthAlgorithmSHA256
+			a = headers.Authenticate{
+				Method:    headers.AuthMethodDigest,
+				Realm:     realm,
+				Nonce:     nonce,
+				Algorithm: &aa,
+			}.Marshal()
+		}
+
+		ret = append(ret, a...)
 	}
+
 	return ret
 }
diff --git a/pkg/headers/authenticate.go b/pkg/headers/authenticate.go
index 0b85c8cf..3a010a93 100644
--- a/pkg/headers/authenticate.go
+++ b/pkg/headers/authenticate.go
@@ -11,34 +11,31 @@ import (
 // AuthMethod is an authentication method.
 type AuthMethod int
 
+// authentication methods.
 const (
-	// AuthBasic is the Basic authentication method
-	AuthBasic AuthMethod = iota
-
-	// AuthDigestMD5 is the Digest authentication method with the MD5 hash
-	AuthDigestMD5
-
-	// AuthDigestSHA256 is the Digest authentication method with the SHA-256 hash
-	AuthDigestSHA256
+	AuthMethodBasic AuthMethod = iota
+	AuthMethodDigest
 )
 
+// AuthAlgorithm is a digest algorithm.
+type AuthAlgorithm int
+
+// digest algorithms.
 const (
-	// AuthDigest is an alias for AuthDigestMD5
-	//
-	// Deprecated: replaced by AuthDigestMD5
-	AuthDigest = AuthDigestMD5
+	AuthAlgorithmMD5 AuthAlgorithm = iota
+	AuthAlgorithmSHA256
 )
 
-func algorithmToMethod(v *string) (AuthMethod, error) {
+func parseAuthAlgorithm(v string) (AuthAlgorithm, error) {
 	switch {
-	case v == nil, strings.ToLower(*v) == "md5":
-		return AuthDigestMD5, nil
+	case strings.ToLower(v) == "md5":
+		return AuthAlgorithmMD5, nil
 
-	case strings.ToLower(*v) == "sha-256":
-		return AuthDigestSHA256, nil
+	case strings.ToLower(v) == "sha-256":
+		return AuthAlgorithmSHA256, nil
 
 	default:
-		return 0, fmt.Errorf("unrecognized algorithm: %v", *v)
+		return 0, fmt.Errorf("unrecognized algorithm: %v", v)
 	}
 }
 
@@ -62,6 +59,9 @@ type Authenticate struct {
 
 	// stale
 	Stale *string
+
+	// algorithm
+	Algorithm *AuthAlgorithm
 }
 
 // Unmarshal decodes a WWW-Authenticate header.
@@ -82,20 +82,18 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
 	}
 	method, v0 := v0[:i], v0[i+1:]
 
-	isDigest := false
-
 	switch method {
 	case "Basic":
-		h.Method = AuthBasic
+		h.Method = AuthMethodBasic
 
 	case "Digest":
-		isDigest = true
+		h.Method = AuthMethodDigest
 
 	default:
 		return fmt.Errorf("invalid method (%s)", method)
 	}
 
-	if !isDigest {
+	if h.Method == AuthMethodBasic {
 		kvs, err := keyValParse(v0, ',')
 		if err != nil {
 			return err
@@ -123,7 +121,6 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
 
 		realmReceived := false
 		nonceReceived := false
-		var algorithm *string
 
 		for k, rv := range kvs {
 			v := rv
@@ -144,18 +141,17 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
 				h.Stale = &v
 
 			case "algorithm":
-				algorithm = &v
+				a, err := parseAuthAlgorithm(v)
+				if err != nil {
+					return err
+				}
+				h.Algorithm = &a
 			}
 		}
 
 		if !realmReceived || !nonceReceived {
 			return fmt.Errorf("one or more digest fields are missing")
 		}
-
-		h.Method, err = algorithmToMethod(algorithm)
-		if err != nil {
-			return err
-		}
 	}
 
 	return nil
@@ -163,7 +159,7 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
 
 // Marshal encodes a WWW-Authenticate header.
 func (h Authenticate) Marshal() base.HeaderValue {
-	if h.Method == AuthBasic {
+	if h.Method == AuthMethodBasic {
 		return base.HeaderValue{"Basic " +
 			"realm=\"" + h.Realm + "\""}
 	}
@@ -178,10 +174,12 @@ func (h Authenticate) Marshal() base.HeaderValue {
 		ret += ", stale=\"" + *h.Stale + "\""
 	}
 
-	if h.Method == AuthDigestMD5 {
-		ret += ", algorithm=\"MD5\""
-	} else {
-		ret += ", algorithm=\"SHA-256\""
+	if h.Algorithm != nil {
+		if *h.Algorithm == AuthAlgorithmMD5 {
+			ret += ", algorithm=\"MD5\""
+		} else {
+			ret += ", algorithm=\"SHA-256\""
+		}
 	}
 
 	return base.HeaderValue{ret}
diff --git a/pkg/headers/authorization.go b/pkg/headers/authorization.go
index b65761bc..e20dd97e 100644
--- a/pkg/headers/authorization.go
+++ b/pkg/headers/authorization.go
@@ -44,6 +44,9 @@ type Authorization struct {
 
 	// opaque
 	Opaque *string
+
+	// algorithm
+	Algorithm *AuthAlgorithm
 }
 
 // Unmarshal decodes an Authorization header.
@@ -64,20 +67,18 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
 	}
 	method, v0 := v0[:i], v0[i+1:]
 
-	isDigest := false
-
 	switch method {
 	case "Basic":
-		h.Method = AuthBasic
+		h.Method = AuthMethodBasic
 
 	case "Digest":
-		isDigest = true
+		h.Method = AuthMethodDigest
 
 	default:
 		return fmt.Errorf("invalid method (%s)", method)
 	}
 
-	if !isDigest {
+	if h.Method == AuthMethodBasic {
 		tmp, err := base64.StdEncoding.DecodeString(v0)
 		if err != nil {
 			return fmt.Errorf("invalid value")
@@ -100,7 +101,6 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
 		nonceReceived := false
 		uriReceived := false
 		responseReceived := false
-		var algorithm *string
 
 		for k, rv := range kvs {
 			v := rv
@@ -130,18 +130,17 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
 				h.Opaque = &v
 
 			case "algorithm":
-				algorithm = &v
+				a, err := parseAuthAlgorithm(v)
+				if err != nil {
+					return err
+				}
+				h.Algorithm = &a
 			}
 		}
 
 		if !realmReceived || !usernameReceived || !nonceReceived || !uriReceived || !responseReceived {
 			return fmt.Errorf("one or more digest fields are missing")
 		}
-
-		h.Method, err = algorithmToMethod(algorithm)
-		if err != nil {
-			return err
-		}
 	}
 
 	return nil
@@ -149,7 +148,7 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
 
 // Marshal encodes an Authorization header.
 func (h Authorization) Marshal() base.HeaderValue {
-	if h.Method == AuthBasic {
+	if h.Method == AuthMethodBasic {
 		return base.HeaderValue{"Basic " +
 			base64.StdEncoding.EncodeToString([]byte(h.BasicUser+":"+h.BasicPass))}
 	}
@@ -162,10 +161,12 @@ func (h Authorization) Marshal() base.HeaderValue {
 		ret += ", opaque=\"" + *h.Opaque + "\""
 	}
 
-	if h.Method == AuthDigestMD5 {
-		ret += ", algorithm=\"MD5\""
-	} else {
-		ret += ", algorithm=\"SHA-256\""
+	if h.Algorithm != nil {
+		if *h.Algorithm == AuthAlgorithmMD5 {
+			ret += ", algorithm=\"MD5\""
+		} else {
+			ret += ", algorithm=\"SHA-256\""
+		}
 	}
 
 	return base.HeaderValue{ret}


Test patch:
diff --git a/client_play_test.go b/client_play_test.go
index cc69a8f8..9b5ece83 100644
--- a/client_play_test.go
+++ b/client_play_test.go
@@ -1705,7 +1705,7 @@ func TestClientPlayRedirect(t *testing.T) {
 							err2 = conn.WriteResponse(&base.Response{
 								Header: base.Header{
 									"WWW-Authenticate": headers.Authenticate{
-										Method: headers.AuthDigestMD5,
+										Method: headers.AuthMethodDigest,
 										Realm:  authRealm,
 										Nonce:  authNonce,
 										Opaque: &authOpaque,
diff --git a/pkg/auth/auth_test.go b/pkg/auth/auth_test.go
index 79f0acc6..e0d5c36b 100644
--- a/pkg/auth/auth_test.go
+++ b/pkg/auth/auth_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
-	"github.com/bluenviron/gortsplib/v4/pkg/headers"
 )
 
 func mustParseURL(s string) *base.URL {
@@ -17,22 +16,22 @@ func mustParseURL(s string) *base.URL {
 	return u
 }
 
-func TestAuth(t *testing.T) {
+func TestCombined(t *testing.T) {
 	for _, c1 := range []struct {
 		name    string
-		methods []headers.AuthMethod
+		methods []ValidateMethod
 	}{
 		{
 			"basic",
-			[]headers.AuthMethod{headers.AuthBasic},
+			[]ValidateMethod{ValidateMethodBasic},
 		},
 		{
 			"digest md5",
-			[]headers.AuthMethod{headers.AuthDigestMD5},
+			[]ValidateMethod{ValidateMethodDigestMD5},
 		},
 		{
 			"digest sha256",
-			[]headers.AuthMethod{headers.AuthDigestSHA256},
+			[]ValidateMethod{ValidateMethodSHA256},
 		},
 		{
 			"all",
@@ -93,38 +92,3 @@ func TestAuth(t *testing.T) {
 		}
 	}
 }
-
-func TestAuthVLC(t *testing.T) {
-	for _, ca := range []struct {
-		baseURL  string
-		mediaURL string
-	}{
-		{
-			"rtsp://myhost/mypath/",
-			"rtsp://myhost/mypath/trackID=0",
-		},
-		{
-			"rtsp://myhost/mypath/test?testing/",
-			"rtsp://myhost/mypath/test?testing/trackID=0",
-		},
-	} {
-		nonce, err := GenerateNonce()
-		require.NoError(t, err)
-
-		se, err := NewSender(
-			GenerateWWWAuthenticate(nil, "IPCAM", nonce),
-			"testuser",
-			"testpass")
-		require.NoError(t, err)
-
-		req := &base.Request{
-			Method: base.Setup,
-			URL:    mustParseURL(ca.baseURL),
-		}
-		se.AddAuthorization(req)
-		req.URL = mustParseURL(ca.mediaURL)
-
-		err = Validate(req, "testuser", "testpass", nil, "IPCAM", nonce)
-		require.NoError(t, err)
-	}
-}
diff --git a/pkg/auth/sender_test.go b/pkg/auth/sender_test.go
index 83defa6c..d72b0c1b 100644
--- a/pkg/auth/sender_test.go
+++ b/pkg/auth/sender_test.go
@@ -4,8 +4,98 @@ import (
 	"testing"
 
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
+	"github.com/stretchr/testify/require"
 )
 
+func TestSender(t *testing.T) {
+	for _, ca := range []struct {
+		name            string
+		wwwAuthenticate base.HeaderValue
+		authorization   base.HeaderValue
+	}{
+		{
+			"basic",
+			base.HeaderValue{
+				"Basic realm=testrealm",
+			},
+			base.HeaderValue{
+				"Basic bXl1c2VyOm15cGFzcw==",
+			},
+		},
+		{
+			"digest md5 implicit",
+			base.HeaderValue{
+				`Digest realm="myrealm", nonce="f49ac6dd0ba708d4becddc9692d1f2ce"`,
+			},
+			base.HeaderValue{
+				"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
+					"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", response=\"ba6e9cccbfeb38db775378a0a9067ba5\"",
+			},
+		},
+		{
+			"digest md5 explicit",
+			base.HeaderValue{
+				`Digest realm="myrealm", nonce="f49ac6dd0ba708d4becddc9692d1f2ce", algorithm="MD5"`,
+			},
+			base.HeaderValue{
+				"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
+					"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", response=\"ba6e9cccbfeb38db775378a0a9067ba5\", " +
+					"algorithm=\"MD5\"",
+			},
+		},
+		{
+			"digest sha256",
+			base.HeaderValue{
+				`Digest realm="myrealm", nonce="f49ac6dd0ba708d4becddc9692d1f2ce", algorithm="SHA-256"`,
+			},
+			base.HeaderValue{
+				"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
+					"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", " +
+					"response=\"e298296ce35c9ab79699c8f3f9508944c1be9395e892f8205b6d66f1b8e663ee\", " +
+					"algorithm=\"SHA-256\"",
+			},
+		},
+		{
+			"multiple 1",
+			base.HeaderValue{
+				"Basic realm=testrealm",
+				`Digest realm="myrealm", nonce="f49ac6dd0ba708d4becddc9692d1f2ce"`,
+			},
+			base.HeaderValue{
+				"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
+					"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", response=\"ba6e9cccbfeb38db775378a0a9067ba5\"",
+			},
+		},
+		{
+			"multiple 2",
+			base.HeaderValue{
+				"Basic realm=testrealm",
+				`Digest realm="myrealm", nonce="f49ac6dd0ba708d4becddc9692d1f2ce", algorithm="MD5"`,
+				`Digest realm="myrealm", nonce="f49ac6dd0ba708d4becddc9692d1f2ce", algorithm="SHA-256"`,
+			},
+			base.HeaderValue{
+				"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
+					"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", " +
+					"response=\"e298296ce35c9ab79699c8f3f9508944c1be9395e892f8205b6d66f1b8e663ee\", " +
+					"algorithm=\"SHA-256\"",
+			},
+		},
+	} {
+		t.Run(ca.name, func(t *testing.T) {
+			se, err := NewSender(ca.wwwAuthenticate, "myuser", "mypass")
+			require.NoError(t, err)
+
+			req := &base.Request{
+				Method: base.Setup,
+				URL:    mustParseURL("rtsp://myhost/mypath?key=val/trackID=3"),
+			}
+			se.AddAuthorization(req)
+
+			require.Equal(t, ca.authorization, req.Header["Authorization"])
+		})
+	}
+}
+
 func FuzzSender(f *testing.F) {
 	f.Add(`Invalid`)
 	f.Add(`Digest`)
diff --git a/pkg/auth/validate_test.go b/pkg/auth/validate_test.go
index 2484682f..b75f200f 100644
--- a/pkg/auth/validate_test.go
+++ b/pkg/auth/validate_test.go
@@ -1,13 +1,88 @@
 package auth
 
 import (
+	"fmt"
 	"testing"
 
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
-	"github.com/bluenviron/gortsplib/v4/pkg/headers"
 	"github.com/stretchr/testify/require"
 )
 
+func TestValidate(t *testing.T) {
+	for _, ca := range []struct {
+		name          string
+		authorization base.HeaderValue
+	}{
+		{
+			"basic",
+			base.HeaderValue{
+				"Basic bXl1c2VyOm15cGFzcw==",
+			},
+		},
+		{
+			"digest md5 implicit",
+			base.HeaderValue{
+				"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
+					"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", response=\"ba6e9cccbfeb38db775378a0a9067ba5\"",
+			},
+		},
+		{
+			"digest md5 explicit",
+			base.HeaderValue{
+				"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
+					"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", response=\"ba6e9cccbfeb38db775378a0a9067ba5\", " +
+					"algorithm=\"MD5\"",
+			},
+		},
+		{
+			"digest sha256",
+			base.HeaderValue{
+				"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
+					"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", " +
+					"response=\"e298296ce35c9ab79699c8f3f9508944c1be9395e892f8205b6d66f1b8e663ee\", " +
+					"algorithm=\"SHA-256\"",
+			},
+		},
+		{
+			"digest vlc",
+			base.HeaderValue{
+				"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
+					"uri=\"rtsp://myhost/mypath?key=val/\", response=\"5ca5ceeca20a05e9a3f49ecde4b42655\"",
+			},
+		},
+	} {
+		t.Run(ca.name, func(t *testing.T) {
+			se, err := NewSender(
+				GenerateWWWAuthenticate([]ValidateMethod{ValidateMethodDigestMD5}, "myrealm", "f49ac6dd0ba708d4becddc9692d1f2ce"),
+				"myuser",
+				"mypass")
+			require.NoError(t, err)
+			req1 := &base.Request{
+				Method: base.Setup,
+				URL:    mustParseURL("rtsp://myhost/mypath?key=val/"),
+			}
+			se.AddAuthorization(req1)
+			fmt.Println(req1.Header)
+
+			req := &base.Request{
+				Method: base.Setup,
+				URL:    mustParseURL("rtsp://myhost/mypath?key=val/trackID=3"),
+				Header: base.Header{
+					"Authorization": ca.authorization,
+				},
+			}
+			err = Validate(
+				req,
+				"myuser",
+				"mypass",
+				nil,
+				"myrealm",
+				"f49ac6dd0ba708d4becddc9692d1f2ce")
+			require.NoError(t, err)
+		})
+	}
+}
+
 func FuzzValidate(f *testing.F) {
 	f.Add(`Invalid`)
 	f.Add(`Digest `)
@@ -35,21 +110,3 @@ func FuzzValidate(f *testing.F) {
 		)
 	})
 }
-
-func TestValidateAdditionalErrors(t *testing.T) {
-	err := Validate(
-		&base.Request{
-			Method: base.Describe,
-			URL:    nil,
-			Header: base.Header{
-				"Authorization": base.HeaderValue{"Basic bXl1c2VyOm15cGFzcw=="},
-			},
-		},
-		"myuser",
-		"mypass",
-		[]headers.AuthMethod{headers.AuthDigestMD5},
-		"IPCAM",
-		"abcde",
-	)
-	require.Error(t, err)
-}
diff --git a/pkg/headers/authenticate_test.go b/pkg/headers/authenticate_test.go
index e9932156..22041749 100644
--- a/pkg/headers/authenticate_test.go
+++ b/pkg/headers/authenticate_test.go
@@ -23,7 +23,7 @@ var casesAuthenticate = []struct {
 		base.HeaderValue{`Basic realm="4419b63f5e51"`},
 		base.HeaderValue{`Basic realm="4419b63f5e51"`},
 		Authenticate{
-			Method: AuthBasic,
+			Method: AuthMethodBasic,
 			Realm:  "4419b63f5e51",
 		},
 	},
@@ -31,9 +31,9 @@ var casesAuthenticate = []struct {
 		"digest 1",
 		base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`},
 		base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", ` +
-			`stale="FALSE", algorithm="MD5"`},
+			`stale="FALSE"`},
 		Authenticate{
-			Method: AuthDigestMD5,
+			Method: AuthMethodDigest,
 			Realm:  "4419b63f5e51",
 			Nonce:  "8b84a3b789283a8bea8da7fa7d41f08b",
 			Stale:  stringPtr("FALSE"),
@@ -43,9 +43,9 @@ var casesAuthenticate = []struct {
 		"digest 2",
 		base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale=FALSE`},
 		base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", ` +
-			`stale="FALSE", algorithm="MD5"`},
+			`stale="FALSE"`},
 		Authenticate{
-			Method: AuthDigestMD5,
+			Method: AuthMethodDigest,
 			Realm:  "4419b63f5e51",
 			Nonce:  "8b84a3b789283a8bea8da7fa7d41f08b",
 			Stale:  stringPtr("FALSE"),
@@ -55,9 +55,9 @@ var casesAuthenticate = []struct {
 		"digest 3",
 		base.HeaderValue{`Digest realm="4419b63f5e51",nonce="133767111917411116111311118211673010032",  stale="FALSE"`},
 		base.HeaderValue{`Digest realm="4419b63f5e51", nonce="133767111917411116111311118211673010032", ` +
-			`stale="FALSE", algorithm="MD5"`},
+			`stale="FALSE"`},
 		Authenticate{
-			Method: AuthDigestMD5,
+			Method: AuthMethodDigest,
 			Realm:  "4419b63f5e51",
 			Nonce:  "133767111917411116111311118211673010032",
 			Stale:  stringPtr("FALSE"),
@@ -66,17 +66,31 @@ var casesAuthenticate = []struct {
 	{
 		"digest after failed auth",
 		base.HeaderValue{`Digest realm="Please log in with a valid username",` +
-			`nonce="752a62306daf32b401a41004555c7663",opaque="",stale=FALSE,algorithm=MD5`},
+			`nonce="752a62306daf32b401a41004555c7663",opaque="",stale=FALSE`},
 		base.HeaderValue{`Digest realm="Please log in with a valid username", ` +
-			`nonce="752a62306daf32b401a41004555c7663", opaque="", stale="FALSE", algorithm="MD5"`},
+			`nonce="752a62306daf32b401a41004555c7663", opaque="", stale="FALSE"`},
 		Authenticate{
-			Method: AuthDigestMD5,
+			Method: AuthMethodDigest,
 			Realm:  "Please log in with a valid username",
 			Nonce:  "752a62306daf32b401a41004555c7663",
 			Opaque: stringPtr(""),
 			Stale:  stringPtr("FALSE"),
 		},
 	},
+	{
+		"digest md5 explicit",
+		base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", ` +
+			`stale="FALSE", algorithm="MD5"`},
+		base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", ` +
+			`stale="FALSE", algorithm="MD5"`},
+		Authenticate{
+			Method:    AuthMethodDigest,
+			Realm:     "4419b63f5e51",
+			Nonce:     "8b84a3b789283a8bea8da7fa7d41f08b",
+			Stale:     stringPtr("FALSE"),
+			Algorithm: algorithmPtr(AuthAlgorithmMD5),
+		},
+	},
 	{
 		"digest sha256",
 		base.HeaderValue{`Digest realm="IP Camera(AB705)", ` +
@@ -84,10 +98,11 @@ var casesAuthenticate = []struct {
 		base.HeaderValue{`Digest realm="IP Camera(AB705)", ` +
 			`nonce="fcc86deace979a488b2bfb89f4d0812c", stale="FALSE", algorithm="SHA-256"`},
 		Authenticate{
-			Method: AuthDigestSHA256,
-			Realm:  "IP Camera(AB705)",
-			Nonce:  "fcc86deace979a488b2bfb89f4d0812c",
-			Stale:  stringPtr("FALSE"),
+			Method:    AuthMethodDigest,
+			Realm:     "IP Camera(AB705)",
+			Nonce:     "fcc86deace979a488b2bfb89f4d0812c",
+			Stale:     stringPtr("FALSE"),
+			Algorithm: algorithmPtr(AuthAlgorithmSHA256),
 		},
 	},
 }
diff --git a/pkg/headers/authorization_test.go b/pkg/headers/authorization_test.go
index a7556224..9ba6696e 100644
--- a/pkg/headers/authorization_test.go
+++ b/pkg/headers/authorization_test.go
@@ -8,6 +8,10 @@ import (
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
 )
 
+func algorithmPtr(v AuthAlgorithm) *AuthAlgorithm {
+	return &v
+}
+
 var casesAuthorization = []struct {
 	name string
 	vin  base.HeaderValue
@@ -19,7 +23,7 @@ var casesAuthorization = []struct {
 		base.HeaderValue{"Basic bXl1c2VyOm15cGFzcw=="},
 		base.HeaderValue{"Basic bXl1c2VyOm15cGFzcw=="},
 		Authorization{
-			Method:    AuthBasic,
+			Method:    AuthMethodBasic,
 			BasicUser: "myuser",
 			BasicPass: "mypass",
 		},
@@ -31,9 +35,9 @@ var casesAuthorization = []struct {
 			`uri="/dir/index.html", response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41"`},
 		base.HeaderValue{`Digest username="Mufasa", realm="testrealm@host.com", ` +
 			`nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", ` +
-			`response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41", algorithm="MD5"`},
+			`response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41"`},
 		Authorization{
-			Method:   AuthDigestMD5,
+			Method:   AuthMethodDigest,
 			Username: "Mufasa",
 			Realm:    "testrealm@host.com",
 			Nonce:    "dcd98b7102dd2f0e8b11d0f600bfb0c093",
@@ -49,9 +53,9 @@ var casesAuthorization = []struct {
 			`response="c072ae90eb4a27f4cdcb90d62266b2a1"`},
 		base.HeaderValue{`Digest username="", realm="IPCAM", ` +
 			`nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` +
-			`response="c072ae90eb4a27f4cdcb90d62266b2a1", algorithm="MD5"`},
+			`response="c072ae90eb4a27f4cdcb90d62266b2a1"`},
 		Authorization{
-			Method:   AuthDigestMD5,
+			Method:   AuthMethodDigest,
 			Username: "",
 			Realm:    "IPCAM",
 			Nonce:    "5d17cd12b9fa8a85ac5ceef0926ea5a6",
@@ -59,6 +63,26 @@ var casesAuthorization = []struct {
 			Response: "c072ae90eb4a27f4cdcb90d62266b2a1",
 		},
 	},
+	{
+		"digest explicit md5",
+		base.HeaderValue{`Digest username="Mufasa", realm="testrealm@host.com", ` +
+			`nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", ` +
+			`uri="/dir/index.html", response="e966c932a9242554e42c8ee200cec7f6", ` +
+			`opaque="5ccc069c403ebaf9f0171e9517f40e41", algorithm="MD5"`},
+		base.HeaderValue{`Digest username="Mufasa", realm="testrealm@host.com", ` +
+			`nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", ` +
+			`response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41", algorithm="MD5"`},
+		Authorization{
+			Method:    AuthMethodDigest,
+			Username:  "Mufasa",
+			Realm:     "testrealm@host.com",
+			Nonce:     "dcd98b7102dd2f0e8b11d0f600bfb0c093",
+			URI:       "/dir/index.html",
+			Response:  "e966c932a9242554e42c8ee200cec7f6",
+			Opaque:    stringPtr("5ccc069c403ebaf9f0171e9517f40e41"),
+			Algorithm: algorithmPtr(AuthAlgorithmMD5),
+		},
+	},
 	{
 		"digest sha256",
 		base.HeaderValue{`Digest username="admin", realm="IP Camera(AB705)", ` +
@@ -68,12 +92,13 @@ var casesAuthorization = []struct {
 			`nonce="1ad195c2b2ca5a03784e53f88e16f579", uri="rtsp://192.168.80.76/", ` +
 			`response="9e2324f104f3ce507d17e44a78fc1293001fe84805bde65d2aaa9be97a5a8913", algorithm="SHA-256"`},
 		Authorization{
-			Method:   AuthDigestSHA256,
-			Username: "admin",
-			Realm:    "IP Camera(AB705)",
-			Nonce:    "1ad195c2b2ca5a03784e53f88e16f579",
-			URI:      "rtsp://192.168.80.76/",
-			Response: "9e2324f104f3ce507d17e44a78fc1293001fe84805bde65d2aaa9be97a5a8913",
+			Method:    AuthMethodDigest,
+			Username:  "admin",
+			Realm:     "IP Camera(AB705)",
+			Nonce:     "1ad195c2b2ca5a03784e53f88e16f579",
+			URI:       "rtsp://192.168.80.76/",
+			Response:  "9e2324f104f3ce507d17e44a78fc1293001fe84805bde65d2aaa9be97a5a8913",
+			Algorithm: algorithmPtr(AuthAlgorithmSHA256),
 		},
 	},
 }