split headers.Authenticate and headers.Authorization (#523)

This commit is contained in:
Alessandro Ros
2024-02-22 13:19:31 +01:00
committed by GitHub
parent c93d5c54d9
commit c10f7aaedb
21 changed files with 369 additions and 424 deletions

View File

@@ -19,37 +19,32 @@ const (
AuthDigest
)
// Authenticate is an Authenticate or a WWW-Authenticate header.
// Authenticate is a WWW-Authenticate header.
type Authenticate struct {
// authentication method
Method AuthMethod
// (optional) username
Username *string
// realm
Realm string
// (optional) realm
Realm *string
//
// Digest authentication fields
//
// (optional) nonce
Nonce *string
// nonce
Nonce string
// (optional) uri
URI *string
// (optional) response
Response *string
// (optional) opaque
// opaque
Opaque *string
// (optional) stale
// stale
Stale *string
// (optional) algorithm
// algorithm
Algorithm *string
}
// Unmarshal decodes an Authenticate or a WWW-Authenticate header.
// Unmarshal decodes a WWW-Authenticate header.
func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
if len(v) == 0 {
return fmt.Errorf("value not provided")
@@ -78,93 +73,86 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
return fmt.Errorf("invalid method (%s)", method)
}
kvs, err := keyValParse(v0, ',')
if err != nil {
return err
}
if h.Method == AuthBasic {
kvs, err := keyValParse(v0, ',')
if err != nil {
return err
}
for k, rv := range kvs {
v := rv
realmReceived := false
switch k {
case "username":
h.Username = &v
for k, rv := range kvs {
v := rv
case "realm":
h.Realm = &v
if k == "realm" {
h.Realm = v
realmReceived = true
}
}
case "nonce":
h.Nonce = &v
if !realmReceived {
return fmt.Errorf("realm is missing")
}
} else { // digest
kvs, err := keyValParse(v0, ',')
if err != nil {
return err
}
case "uri":
h.URI = &v
realmReceived := false
nonceReceived := false
case "response":
h.Response = &v
for k, rv := range kvs {
v := rv
case "opaque":
h.Opaque = &v
switch k {
case "realm":
h.Realm = v
realmReceived = true
case "stale":
h.Stale = &v
case "nonce":
h.Nonce = v
nonceReceived = true
case "algorithm":
h.Algorithm = &v
case "opaque":
h.Opaque = &v
case "stale":
h.Stale = &v
case "algorithm":
h.Algorithm = &v
}
}
if !realmReceived || !nonceReceived {
return fmt.Errorf("one or more digest fields are missing")
}
}
return nil
}
// Marshal encodes an Authenticate or a WWW-Authenticate header.
// Marshal encodes a WWW-Authenticate header.
func (h Authenticate) Marshal() base.HeaderValue {
ret := ""
switch h.Method {
case AuthBasic:
ret += "Basic"
case AuthDigest:
ret += "Digest"
if h.Method == AuthBasic {
return base.HeaderValue{"Basic " +
"realm=\"" + h.Realm + "\""}
}
ret += " "
var rets []string
if h.Username != nil {
rets = append(rets, "username=\""+*h.Username+"\"")
}
if h.Realm != nil {
rets = append(rets, "realm=\""+*h.Realm+"\"")
}
if h.Nonce != nil {
rets = append(rets, "nonce=\""+*h.Nonce+"\"")
}
if h.URI != nil {
rets = append(rets, "uri=\""+*h.URI+"\"")
}
if h.Response != nil {
rets = append(rets, "response=\""+*h.Response+"\"")
}
ret := "Digest realm=\"" + h.Realm + "\", nonce=\"" + h.Nonce + "\""
if h.Opaque != nil {
rets = append(rets, "opaque=\""+*h.Opaque+"\"")
ret += ", opaque=\"" + *h.Opaque + "\""
}
if h.Stale != nil {
rets = append(rets, "stale=\""+*h.Stale+"\"")
ret += ", stale=\"" + *h.Stale + "\""
}
if h.Algorithm != nil {
rets = append(rets, "algorithm=\""+*h.Algorithm+"\"")
ret += ", algorithm=\"" + *h.Algorithm + "\""
}
ret += strings.Join(rets, ", ")
return base.HeaderValue{ret}
}

View File

@@ -24,82 +24,52 @@ var casesAuthenticate = []struct {
base.HeaderValue{`Basic realm="4419b63f5e51"`},
Authenticate{
Method: AuthBasic,
Realm: stringPtr("4419b63f5e51"),
Realm: "4419b63f5e51",
},
},
{
"digest request 1",
"digest 1",
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`},
Authenticate{
Method: AuthDigest,
Realm: stringPtr("4419b63f5e51"),
Nonce: stringPtr("8b84a3b789283a8bea8da7fa7d41f08b"),
Realm: "4419b63f5e51",
Nonce: "8b84a3b789283a8bea8da7fa7d41f08b",
Stale: stringPtr("FALSE"),
},
},
{
"digest request 2",
"digest 2",
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale=FALSE`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`},
Authenticate{
Method: AuthDigest,
Realm: stringPtr("4419b63f5e51"),
Nonce: stringPtr("8b84a3b789283a8bea8da7fa7d41f08b"),
Realm: "4419b63f5e51",
Nonce: "8b84a3b789283a8bea8da7fa7d41f08b",
Stale: stringPtr("FALSE"),
},
},
{
"digest request 3",
"digest 3",
base.HeaderValue{`Digest realm="4419b63f5e51",nonce="133767111917411116111311118211673010032", stale="FALSE"`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="133767111917411116111311118211673010032", stale="FALSE"`},
Authenticate{
Method: AuthDigest,
Realm: stringPtr("4419b63f5e51"),
Nonce: stringPtr("133767111917411116111311118211673010032"),
Realm: "4419b63f5e51",
Nonce: "133767111917411116111311118211673010032",
Stale: stringPtr("FALSE"),
},
},
{
"digest response generic",
base.HeaderValue{`Digest username="aa", realm="bb", nonce="cc", uri="dd", response="ee"`},
base.HeaderValue{`Digest username="aa", realm="bb", nonce="cc", uri="dd", response="ee"`},
Authenticate{
Method: AuthDigest,
Username: stringPtr("aa"),
Realm: stringPtr("bb"),
Nonce: stringPtr("cc"),
URI: stringPtr("dd"),
Response: stringPtr("ee"),
},
},
{
"digest response with empty field",
base.HeaderValue{`Digest username="", realm="IPCAM", ` +
`nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` +
`response="c072ae90eb4a27f4cdcb90d62266b2a1"`},
base.HeaderValue{`Digest username="", realm="IPCAM", ` +
`nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` +
`response="c072ae90eb4a27f4cdcb90d62266b2a1"`},
Authenticate{
Method: AuthDigest,
Username: stringPtr(""),
Realm: stringPtr("IPCAM"),
Nonce: stringPtr("5d17cd12b9fa8a85ac5ceef0926ea5a6"),
URI: stringPtr("rtsp://localhost:8554/mystream"),
Response: stringPtr("c072ae90eb4a27f4cdcb90d62266b2a1"),
},
},
{
"digest response with no spaces and additional fields",
"digest after failed auth",
base.HeaderValue{`Digest realm="Please log in with a valid username",` +
`nonce="752a62306daf32b401a41004555c7663",opaque="",stale=FALSE,algorithm=MD5`},
base.HeaderValue{`Digest realm="Please log in with a valid username", ` +
`nonce="752a62306daf32b401a41004555c7663", opaque="", stale="FALSE", algorithm="MD5"`},
Authenticate{
Method: AuthDigest,
Realm: stringPtr("Please log in with a valid username"),
Nonce: stringPtr("752a62306daf32b401a41004555c7663"),
Realm: "Please log in with a valid username",
Nonce: "752a62306daf32b401a41004555c7663",
Opaque: stringPtr(""),
Stale: stringPtr("FALSE"),
Algorithm: stringPtr("MD5"),
@@ -118,46 +88,6 @@ func TestAuthenticateUnmarshal(t *testing.T) {
}
}
func TestAutenticatehUnmarshalErrors(t *testing.T) {
for _, ca := range []struct {
name string
hv base.HeaderValue
err string
}{
{
"empty",
base.HeaderValue{},
"value not provided",
},
{
"2 values",
base.HeaderValue{"a", "b"},
"value provided multiple times ([a b])",
},
{
"no keys",
base.HeaderValue{"Basic"},
"unable to split between method and keys (Basic)",
},
{
"invalid keys",
base.HeaderValue{`Basic key1="k`},
"apexes not closed (key1=\"k)",
},
{
"invalid method",
base.HeaderValue{"Testing key1=val1"},
"invalid method (Testing)",
},
} {
t.Run(ca.name, func(t *testing.T) {
var h Authenticate
err := h.Unmarshal(ca.hv)
require.EqualError(t, err, ca.err)
})
}
}
func TestAuthenticateMarshal(t *testing.T) {
for _, ca := range casesAuthenticate {
t.Run(ca.name, func(t *testing.T) {
@@ -166,3 +96,28 @@ func TestAuthenticateMarshal(t *testing.T) {
})
}
}
func FuzzAuthenticateUnmarshal(f *testing.F) {
for _, ca := range casesAuthenticate {
f.Add(ca.vin[0])
}
f.Fuzz(func(t *testing.T, b string) {
var h Authenticate
h.Unmarshal(base.HeaderValue{b}) //nolint:errcheck
})
}
func TestAuthenticateAdditionalErrors(t *testing.T) {
func() {
var h Authenticate
err := h.Unmarshal(base.HeaderValue{})
require.Error(t, err)
}()
func() {
var h Authenticate
err := h.Unmarshal(base.HeaderValue{"a", "b"})
require.Error(t, err)
}()
}

View File

@@ -13,14 +13,37 @@ type Authorization struct {
// authentication method
Method AuthMethod
// basic user
//
// Basic authentication fields
//
// user
BasicUser string
// basic password
// password
BasicPass string
// digest values
DigestValues Authenticate
//
// Digest authentication fields
//
// username
Username string
// realm
Realm string
// nonce
Nonce string
// URI
URI string
// response
Response string
// response
Opaque *string
}
// Unmarshal decodes an Authorization header.
@@ -35,12 +58,24 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
v0 := v[0]
switch {
case strings.HasPrefix(v0, "Basic "):
i := strings.IndexByte(v0, ' ')
if i < 0 {
return fmt.Errorf("unable to split between method and keys (%v)", v0)
}
method, v0 := v0[:i], v0[i+1:]
switch method {
case "Basic":
h.Method = AuthBasic
v0 = v0[len("Basic "):]
case "Digest":
h.Method = AuthDigest
default:
return fmt.Errorf("invalid method (%s)", method)
}
if h.Method == AuthBasic {
tmp, err := base64.StdEncoding.DecodeString(v0)
if err != nil {
return fmt.Errorf("invalid value")
@@ -52,20 +87,50 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
}
h.BasicUser, h.BasicPass = tmp2[0], tmp2[1]
case strings.HasPrefix(v0, "Digest "):
h.Method = AuthDigest
var vals Authenticate
err := vals.Unmarshal(base.HeaderValue{v0})
} else { // digest
kvs, err := keyValParse(v0, ',')
if err != nil {
return err
}
h.DigestValues = vals
realmReceived := false
usernameReceived := false
nonceReceived := false
uriReceived := false
responseReceived := false
default:
return fmt.Errorf("invalid authorization header")
for k, rv := range kvs {
v := rv
switch k {
case "realm":
h.Realm = v
realmReceived = true
case "username":
h.Username = v
usernameReceived = true
case "nonce":
h.Nonce = v
nonceReceived = true
case "uri":
h.URI = v
uriReceived = true
case "response":
h.Response = v
responseReceived = true
case "opaque":
h.Opaque = &v
}
}
if !realmReceived || !usernameReceived || !nonceReceived || !uriReceived || !responseReceived {
return fmt.Errorf("one or more digest fields are missing")
}
}
return nil
@@ -73,13 +138,18 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
// Marshal encodes an Authorization header.
func (h Authorization) Marshal() base.HeaderValue {
switch h.Method {
case AuthBasic:
response := base64.StdEncoding.EncodeToString([]byte(h.BasicUser + ":" + h.BasicPass))
return base.HeaderValue{"Basic " + response}
default: // AuthDigest
return h.DigestValues.Marshal()
if h.Method == AuthBasic {
return base.HeaderValue{"Basic " +
base64.StdEncoding.EncodeToString([]byte(h.BasicUser+":"+h.BasicPass))}
}
ret := "Digest " +
"username=\"" + h.Username + "\", realm=\"" + h.Realm + "\", " +
"nonce=\"" + h.Nonce + "\", uri=\"" + h.URI + "\", response=\"" + h.Response + "\""
if h.Opaque != nil {
ret += ", opaque=\"" + *h.Opaque + "\""
}
return base.HeaderValue{ret}
}

View File

@@ -26,16 +26,37 @@ var casesAuthorization = []struct {
},
{
"digest",
base.HeaderValue{"Digest realm=\"4419b63f5e51\", nonce=\"8b84a3b789283a8bea8da7fa7d41f08b\", stale=\"FALSE\""},
base.HeaderValue{"Digest realm=\"4419b63f5e51\", nonce=\"8b84a3b789283a8bea8da7fa7d41f08b\", stale=\"FALSE\""},
base.HeaderValue{`Digest username="Mufasa", realm="testrealm@host.com", ` +
`nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", ` +
`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"`},
Authorization{
Method: AuthDigest,
DigestValues: Authenticate{
Method: AuthDigest,
Realm: stringPtr("4419b63f5e51"),
Nonce: stringPtr("8b84a3b789283a8bea8da7fa7d41f08b"),
Stale: stringPtr("FALSE"),
},
Method: AuthDigest,
Username: "Mufasa",
Realm: "testrealm@host.com",
Nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093",
URI: "/dir/index.html",
Response: "e966c932a9242554e42c8ee200cec7f6",
Opaque: stringPtr("5ccc069c403ebaf9f0171e9517f40e41"),
},
},
{
"digest with empty field",
base.HeaderValue{`Digest username="", realm="IPCAM", ` +
`nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` +
`response="c072ae90eb4a27f4cdcb90d62266b2a1"`},
base.HeaderValue{`Digest username="", realm="IPCAM", ` +
`nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` +
`response="c072ae90eb4a27f4cdcb90d62266b2a1"`},
Authorization{
Method: AuthDigest,
Username: "",
Realm: "IPCAM",
Nonce: "5d17cd12b9fa8a85ac5ceef0926ea5a6",
URI: "rtsp://localhost:8554/mystream",
Response: "c072ae90eb4a27f4cdcb90d62266b2a1",
},
},
}
@@ -51,51 +72,6 @@ func TestAuthorizationUnmarshal(t *testing.T) {
}
}
func TestAuthorizationUnmarshalErrors(t *testing.T) {
for _, ca := range []struct {
name string
hv base.HeaderValue
err string
}{
{
"empty",
base.HeaderValue{},
"value not provided",
},
{
"2 values",
base.HeaderValue{"a", "b"},
"value provided multiple times ([a b])",
},
{
"invalid",
base.HeaderValue{`Invalid`},
"invalid authorization header",
},
{
"basic invalid 1",
base.HeaderValue{`Basic aaa`},
"invalid value",
},
{
"basic invalid 2",
base.HeaderValue{`Basic aW52YWxpZA==`},
"invalid value",
},
{
"digest invalid",
base.HeaderValue{`Digest test="v`},
"apexes not closed (test=\"v)",
},
} {
t.Run(ca.name, func(t *testing.T) {
var h Authorization
err := h.Unmarshal(ca.hv)
require.EqualError(t, err, ca.err)
})
}
}
func TestAuthorizationMarshal(t *testing.T) {
for _, ca := range casesAuthorization {
t.Run(ca.name, func(t *testing.T) {
@@ -104,3 +80,28 @@ func TestAuthorizationMarshal(t *testing.T) {
})
}
}
func FuzzAuthorizationUnmarshal(f *testing.F) {
for _, ca := range casesAuthorization {
f.Add(ca.vin[0])
}
f.Fuzz(func(t *testing.T, b string) {
var h Authorization
h.Unmarshal(base.HeaderValue{b}) //nolint:errcheck
})
}
func TestAuthorizationAdditionalErrors(t *testing.T) {
func() {
var h Authorization
err := h.Unmarshal(base.HeaderValue{})
require.Error(t, err)
}()
func() {
var h Authorization
err := h.Unmarshal(base.HeaderValue{"a", "b"})
require.Error(t, err)
}()
}

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Basic =\"")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Digest ")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("0")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string(" ")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Digest =\"")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Basic ")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Digest ")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("0")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string(" ")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Digest =\"")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Basic ")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Basic 0")