github.com/google/osv-scalibr@v0.4.1/clients/datasource/http_auth_test.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package datasource_test 16 17 import ( 18 "net/http" 19 "testing" 20 21 "github.com/google/osv-scalibr/clients/datasource" 22 ) 23 24 // mockTransport is used to inspect the requests being made by HTTPAuthentications 25 type mockTransport struct { 26 Requests []*http.Request // All requests made to this transport 27 UnauthedResponse *http.Response // Response sent when request does not have an 'Authorization' header. 28 AuthedReponse *http.Response // Response to sent when request does include 'Authorization' (not checked). 29 } 30 31 func (mt *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { 32 mt.Requests = append(mt.Requests, req) 33 var resp *http.Response 34 if req.Header.Get("Authorization") == "" { 35 resp = mt.UnauthedResponse 36 } else { 37 resp = mt.AuthedReponse 38 } 39 if resp == nil { 40 resp = &http.Response{StatusCode: http.StatusOK} 41 } 42 43 return resp, nil 44 } 45 46 func TestHTTPAuthentication(t *testing.T) { 47 tests := []struct { 48 name string 49 httpAuth *datasource.HTTPAuthentication 50 requestURL string 51 wwwAuth []string 52 expectedAuths []string // expected Authentication headers received. 53 expectedResponseCodes []int // expected final response codes received (length may be less than expectedAuths) 54 }{ 55 { 56 name: "nil auth", 57 httpAuth: nil, 58 requestURL: "http://127.0.0.1/", 59 wwwAuth: []string{"Basic"}, 60 expectedAuths: []string{""}, 61 expectedResponseCodes: []int{http.StatusUnauthorized}, 62 }, 63 { 64 name: "default auth", 65 httpAuth: &datasource.HTTPAuthentication{}, 66 requestURL: "http://127.0.0.1/", 67 wwwAuth: []string{"Basic"}, 68 expectedAuths: []string{""}, 69 expectedResponseCodes: []int{http.StatusUnauthorized}, 70 }, 71 { 72 name: "basic_auth", 73 httpAuth: &datasource.HTTPAuthentication{ 74 SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBasic}, 75 AlwaysAuth: true, 76 Username: "Aladdin", 77 Password: "open sesame", 78 }, 79 requestURL: "http://127.0.0.1/", 80 expectedAuths: []string{"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}, 81 expectedResponseCodes: []int{http.StatusOK}, 82 }, 83 { 84 name: "basic_auth_from_token", 85 httpAuth: &datasource.HTTPAuthentication{ 86 SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBasic}, 87 AlwaysAuth: true, 88 Username: "ignored", 89 Password: "ignored", 90 BasicAuth: "QWxhZGRpbjpvcGVuIHNlc2FtZQ==", 91 }, 92 requestURL: "http://127.0.0.1/", 93 expectedAuths: []string{"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}, 94 expectedResponseCodes: []int{http.StatusOK}, 95 }, 96 { 97 name: "basic_auth_missing_username", 98 httpAuth: &datasource.HTTPAuthentication{ 99 SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBasic}, 100 AlwaysAuth: true, 101 Username: "", 102 Password: "ignored", 103 }, 104 requestURL: "http://127.0.0.1/", 105 expectedAuths: []string{""}, 106 expectedResponseCodes: []int{http.StatusOK}, 107 }, 108 { 109 name: "basic_auth_missing_password", 110 httpAuth: &datasource.HTTPAuthentication{ 111 SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBasic}, 112 AlwaysAuth: true, 113 Username: "ignored", 114 Password: "", 115 }, 116 requestURL: "http://127.0.0.1/", 117 expectedAuths: []string{""}, 118 expectedResponseCodes: []int{http.StatusOK}, 119 }, 120 { 121 name: "basic_auth_not_always", 122 httpAuth: &datasource.HTTPAuthentication{ 123 SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBasic}, 124 AlwaysAuth: false, 125 BasicAuth: "YTph", 126 }, 127 requestURL: "http://127.0.0.1/", 128 wwwAuth: []string{"Basic realm=\"User Visible Realm\""}, 129 expectedAuths: []string{"", "Basic YTph"}, 130 expectedResponseCodes: []int{http.StatusOK}, 131 }, 132 { 133 name: "bearer_auth", 134 httpAuth: &datasource.HTTPAuthentication{ 135 SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBearer}, 136 AlwaysAuth: true, 137 BearerToken: "abcdefgh", 138 }, 139 requestURL: "http://127.0.0.1/", 140 expectedAuths: []string{"Bearer abcdefgh"}, 141 expectedResponseCodes: []int{http.StatusOK}, 142 }, 143 { 144 name: "bearer_auth_not_always", 145 httpAuth: &datasource.HTTPAuthentication{ 146 SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBearer}, 147 AlwaysAuth: false, 148 BearerToken: "abcdefgh", 149 }, 150 requestURL: "http://127.0.0.1/", 151 wwwAuth: []string{"Bearer"}, 152 expectedAuths: []string{"", "Bearer abcdefgh"}, 153 expectedResponseCodes: []int{http.StatusOK}, 154 }, 155 { 156 name: "always_auth_priority", 157 httpAuth: &datasource.HTTPAuthentication{ 158 SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBasic, datasource.AuthBearer}, 159 AlwaysAuth: true, 160 BasicAuth: "UseThisOne", 161 BearerToken: "NotThisOne", 162 }, 163 requestURL: "http://127.0.0.1/", 164 expectedAuths: []string{"Basic UseThisOne"}, 165 expectedResponseCodes: []int{http.StatusOK}, 166 }, 167 { 168 name: "not_always_auth_priority", 169 httpAuth: &datasource.HTTPAuthentication{ 170 SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBearer, datasource.AuthDigest, datasource.AuthBasic}, 171 AlwaysAuth: false, 172 Username: "DoNotUse", 173 Password: "ThisField", 174 BearerToken: "PleaseUseThis", 175 }, 176 requestURL: "http://127.0.0.1/", 177 wwwAuth: []string{"Basic", "Bearer"}, 178 expectedAuths: []string{"", "Bearer PleaseUseThis"}, 179 expectedResponseCodes: []int{http.StatusOK}, 180 }, 181 { 182 name: "digest_auth", 183 // Example from https://en.wikipedia.org/wiki/Digest_access_authentication#Example_with_explanation 184 httpAuth: &datasource.HTTPAuthentication{ 185 SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthDigest}, 186 AlwaysAuth: false, 187 Username: "Mufasa", 188 Password: "Circle Of Life", 189 CnonceFunc: func() string { return "0a4f113b" }, 190 }, 191 requestURL: "https://127.0.0.1/dir/index.html", 192 wwwAuth: []string{ 193 "Digest realm=\"testrealm@host.com\", " + 194 "qop=\"auth,auth-int\", " + 195 "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + 196 "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"", 197 }, 198 expectedAuths: []string{ 199 "", 200 // The order of these fields shouldn't actually matter 201 "Digest username=\"Mufasa\", " + 202 "realm=\"testrealm@host.com\", " + 203 "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + 204 "uri=\"/dir/index.html\", " + 205 "qop=auth, " + 206 "nc=00000001, " + 207 "cnonce=\"0a4f113b\", " + 208 "response=\"6629fae49393a05397450978507c4ef1\", " + 209 "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"", 210 }, 211 expectedResponseCodes: []int{http.StatusOK}, 212 }, 213 { 214 name: "digest_auth_rfc2069", // old spec, without qop header 215 httpAuth: &datasource.HTTPAuthentication{ 216 SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthDigest}, 217 AlwaysAuth: false, 218 Username: "Mufasa", 219 Password: "Circle Of Life", 220 }, 221 requestURL: "https://127.0.0.1/dir/index.html", 222 wwwAuth: []string{ 223 "Digest realm=\"testrealm@host.com\", " + 224 "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + 225 "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"", 226 }, 227 expectedAuths: []string{ 228 "", 229 // The order of these fields shouldn't actually matter 230 "Digest username=\"Mufasa\", " + 231 "realm=\"testrealm@host.com\", " + 232 "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + 233 "uri=\"/dir/index.html\", " + 234 "response=\"670fd8c2df070c60b045671b8b24ff02\", " + 235 "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"", 236 }, 237 expectedResponseCodes: []int{http.StatusOK}, 238 }, 239 { 240 name: "digest_auth_mvn", 241 // From what mvn sends. 242 httpAuth: &datasource.HTTPAuthentication{ 243 SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthDigest}, 244 AlwaysAuth: false, 245 Username: "my-username", 246 Password: "cool-password", 247 CnonceFunc: func() string { return "f7ef2d457dabcd54" }, 248 }, 249 requestURL: "https://127.0.0.1:41565/commons-io/commons-io/1.0/commons-io-1.0.pom", 250 wwwAuth: []string{ 251 "Digest realm=\"test@osv.dev\"," + 252 "qop=\"auth\"," + 253 "nonce=\"deadbeef\"," + 254 "opaque=\"aaaa\"," + 255 "algorithm=\"MD5-sess\"," + 256 "domain=\"/test\"", 257 }, 258 expectedAuths: []string{ 259 "", 260 // The order of these fields shouldn't actually matter 261 "Digest username=\"my-username\", " + 262 "realm=\"test@osv.dev\", " + 263 "nonce=\"deadbeef\", " + 264 "uri=\"/commons-io/commons-io/1.0/commons-io-1.0.pom\", " + 265 "qop=auth, " + 266 "nc=00000001, " + 267 "cnonce=\"f7ef2d457dabcd54\", " + 268 "algorithm=MD5-sess, " + 269 "response=\"15a35e7018a0fc7db05d31185e0d2c9e\", " + 270 "opaque=\"aaaa\"", 271 }, 272 expectedResponseCodes: []int{http.StatusOK}, 273 }, 274 275 { 276 name: "basic_auth_reuse_on_subsequent", 277 httpAuth: &datasource.HTTPAuthentication{ 278 SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthDigest, datasource.AuthBasic}, 279 AlwaysAuth: false, 280 Username: "user", 281 Password: "pass", 282 }, 283 requestURL: "http://127.0.0.1/", 284 wwwAuth: []string{"Basic realm=\"Realm\""}, 285 expectedAuths: []string{"", "Basic dXNlcjpwYXNz", "Basic dXNlcjpwYXNz"}, 286 expectedResponseCodes: []int{http.StatusOK, http.StatusOK}, 287 }, 288 } 289 290 for _, tt := range tests { 291 t.Run(tt.name, func(t *testing.T) { 292 mt := &mockTransport{} 293 if len(tt.wwwAuth) > 0 { 294 mt.UnauthedResponse = &http.Response{ 295 StatusCode: http.StatusUnauthorized, 296 Header: make(http.Header), 297 } 298 for _, v := range tt.wwwAuth { 299 mt.UnauthedResponse.Header.Add("WWW-Authenticate", v) 300 } 301 } 302 httpClient := &http.Client{Transport: mt} 303 for _, want := range tt.expectedResponseCodes { 304 resp, err := tt.httpAuth.Get(t.Context(), httpClient, tt.requestURL) 305 if err != nil { 306 t.Fatalf("error making request: %v", err) 307 } 308 defer resp.Body.Close() 309 if resp.StatusCode != want { 310 t.Errorf("authorization response status code got = %d, want %d", resp.StatusCode, want) 311 } 312 } 313 if len(mt.Requests) != len(tt.expectedAuths) { 314 t.Fatalf("unexpected number of requests got = %d, want %d", len(mt.Requests), len(tt.expectedAuths)) 315 } 316 for i, want := range tt.expectedAuths { 317 got := mt.Requests[i].Header.Get("Authorization") 318 if got != want { 319 t.Errorf("authorization header got = \"%s\", want \"%s\"", got, want) 320 } 321 } 322 }) 323 } 324 }