github.com/greenpau/go-authcrunch@v1.1.4/pkg/authn/serve_http_test.go (about) 1 // Copyright 2022 Paul Greenberg greenpau@outlook.com 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 authn 16 17 import ( 18 "bytes" 19 "context" 20 "crypto/tls" 21 "crypto/x509" 22 "encoding/json" 23 "fmt" 24 "github.com/greenpau/go-authcrunch/internal/tests" 25 "github.com/greenpau/go-authcrunch/internal/testutils" 26 "github.com/greenpau/go-authcrunch/pkg/acl" 27 "github.com/greenpau/go-authcrunch/pkg/ids" 28 // "github.com/greenpau/go-authcrunch/pkg/authn/backends/local" 29 "github.com/greenpau/go-authcrunch/pkg/errors" 30 "github.com/greenpau/go-authcrunch/pkg/requests" 31 logutil "github.com/greenpau/go-authcrunch/pkg/util/log" 32 "io/ioutil" 33 "net" 34 "net/http" 35 "net/http/cookiejar" 36 "net/url" 37 "strconv" 38 "strings" 39 "time" 40 41 "net/http/httptest" 42 "testing" 43 ) 44 45 type testAuthRequest struct { 46 endpoint string 47 username string 48 password string 49 realm string 50 contentType string 51 } 52 53 type testAppRequest struct { 54 id string 55 method string 56 path string 57 headers map[string]string 58 query map[string]string 59 contentType string 60 } 61 62 func TestServeHTTP(t *testing.T) { 63 db, err := testutils.CreateTestDatabase("TestServeHTTP") 64 if err != nil { 65 t.Fatalf("failed to create temp dir: %v", err) 66 } 67 dbPath := db.GetPath() 68 t.Logf("%v", dbPath) 69 70 logger := logutil.NewLogger() 71 cfg := &PortalConfig{ 72 Name: "myportal", 73 AccessListConfigs: []*acl.RuleConfiguration{ 74 { 75 Conditions: []string{ 76 "match roles authp/admin", 77 }, 78 Action: "allow", 79 }, 80 }, 81 IdentityStores: []string{"local_backend"}, 82 } 83 84 storeCfg := &ids.IdentityStoreConfig{ 85 Name: "local_backend", 86 Kind: "local", 87 Params: map[string]interface{}{ 88 "path": dbPath, 89 "realm": "localize", 90 }, 91 } 92 93 store, err := ids.NewIdentityStore(storeCfg, logger) 94 if err != nil { 95 t.Fatal(err) 96 } 97 if err := store.Configure(); err != nil { 98 t.Fatal(err) 99 } 100 101 params := PortalParameters{ 102 Config: cfg, 103 Logger: logger, 104 IdentityStores: []ids.IdentityStore{ 105 store, 106 }, 107 } 108 109 portal, err := NewPortal(params) 110 if err != nil { 111 t.Fatal(err) 112 } 113 114 var testcases = []struct { 115 name string 116 disabled bool 117 auth *testAuthRequest 118 requests []*testAppRequest 119 // Expected results. 120 want map[string]interface{} 121 shouldErr bool 122 err error 123 }{ 124 { 125 name: "test unauthenticated json request to portal page", 126 // disabled: true, 127 requests: []*testAppRequest{ 128 { 129 method: "GET", 130 path: "/auth/", 131 contentType: "application/json", 132 headers: map[string]string{ 133 "Accept": "application/json", 134 }, 135 }, 136 }, 137 want: map[string]interface{}{ 138 "response": requests.Response{ 139 RedirectTokenName: "AUTHP_REDIRECT_URL", 140 }, 141 "status_code": http.StatusUnauthorized, 142 "content_type": "application/json", 143 "message": "Access denied", 144 }, 145 }, 146 { 147 name: "test authenticate json request and get whoami page", 148 //disabled: true, 149 auth: &testAuthRequest{ 150 endpoint: "/auth/login", 151 username: tests.TestUser1, 152 password: tests.TestPwd1, 153 realm: "localize", 154 contentType: "application/json", 155 }, 156 requests: []*testAppRequest{ 157 { 158 method: "GET", 159 path: "/auth/whoami", 160 contentType: "application/json", 161 headers: map[string]string{ 162 "Accept": "application/json", 163 }, 164 }, 165 }, 166 want: map[string]interface{}{ 167 "response": requests.Response{ 168 RedirectTokenName: "AUTHP_REDIRECT_URL", 169 Authenticated: true, 170 }, 171 "status_code": http.StatusOK, 172 "content_type": "application/json", 173 }, 174 }, 175 { 176 name: "test authenticated user accessing css static asset", 177 // disabled: true, 178 auth: &testAuthRequest{ 179 endpoint: "/auth/login", 180 username: tests.TestUser1, 181 password: tests.TestPwd1, 182 realm: "localize", 183 contentType: "application/json", 184 }, 185 requests: []*testAppRequest{ 186 { 187 method: "GET", 188 path: "/auth/assets/css/styles.css", 189 }, 190 }, 191 want: map[string]interface{}{ 192 "response": requests.Response{ 193 RedirectTokenName: "AUTHP_REDIRECT_URL", 194 Authenticated: true, 195 }, 196 "status_code": http.StatusOK, 197 "content_type": "text/css", 198 }, 199 }, 200 { 201 name: "test authenticated user accessing favicon static asset", 202 // disabled: true, 203 auth: &testAuthRequest{ 204 endpoint: "/auth/login", 205 username: tests.TestUser1, 206 password: tests.TestPwd1, 207 realm: "localize", 208 contentType: "application/json", 209 }, 210 requests: []*testAppRequest{ 211 { 212 method: "GET", 213 path: "/favicon.png", 214 }, 215 }, 216 want: map[string]interface{}{ 217 "response": requests.Response{ 218 RedirectTokenName: "AUTHP_REDIRECT_URL", 219 Authenticated: true, 220 }, 221 "status_code": http.StatusOK, 222 "content_type": "image/png", 223 }, 224 }, 225 { 226 name: "test unauthenticated user accessing default portal page", 227 requests: []*testAppRequest{ 228 { 229 method: "GET", 230 path: "/portal", 231 }, 232 }, 233 want: map[string]interface{}{ 234 "response": requests.Response{ 235 RedirectTokenName: "AUTHP_REDIRECT_URL", 236 }, 237 "status_code": http.StatusFound, 238 "content_type": "", 239 }, 240 }, 241 } 242 243 for _, tc := range testcases { 244 t.Run(tc.name, func(t *testing.T) { 245 if tc.disabled { 246 return 247 } 248 msgs := []string{fmt.Sprintf("test name: %s", tc.name)} 249 got := make(map[string]interface{}) 250 // Create test HTTP server. 251 ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 252 rr := requests.NewRequest() 253 err := portal.ServeHTTP(context.Background(), w, r, rr) 254 if tests.EvalErrWithLog(t, err, tc.want, tc.shouldErr, tc.err, msgs) { 255 return 256 } 257 got["response"] = rr.Response 258 })) 259 defer ts.Close() 260 261 cert, err := x509.ParseCertificate(ts.TLS.Certificates[0].Certificate[0]) 262 if err != nil { 263 if tests.EvalErrWithLog(t, errors.ErrGeneric.WithArgs("failed extracting server certs", err), tc.want, tc.shouldErr, tc.err, msgs) { 264 return 265 } 266 } 267 cp := x509.NewCertPool() 268 cp.AddCert(cert) 269 270 cj, err := cookiejar.New(nil) 271 if err != nil { 272 if tests.EvalErrWithLog(t, errors.ErrGeneric.WithArgs("failed adding cookie jar", err), tc.want, tc.shouldErr, tc.err, msgs) { 273 return 274 } 275 } 276 client := http.Client{ 277 Jar: cj, 278 Timeout: time.Second * 10, 279 Transport: &http.Transport{ 280 Dial: (&net.Dialer{ 281 Timeout: 5 * time.Second, 282 }).Dial, 283 TLSHandshakeTimeout: 5 * time.Second, 284 TLSClientConfig: &tls.Config{ 285 RootCAs: cp, 286 }, 287 }, 288 CheckRedirect: func(req *http.Request, via []*http.Request) error { 289 // Do not follow redirects. 290 return http.ErrUseLastResponse 291 }, 292 } 293 294 // Authenticate. 295 if tc.auth != nil { 296 msgs = append(msgs, fmt.Sprintf("Endpoint and content type %s %s", tc.auth.endpoint, tc.auth.contentType)) 297 switch tc.auth.contentType { 298 case "application/json": 299 params := &AuthRequest{ 300 Username: tc.auth.username, 301 Password: tc.auth.password, 302 Realm: tc.auth.realm, 303 } 304 b, _ := json.Marshal(params) 305 req, _ := http.NewRequest("POST", ts.URL+tc.auth.endpoint, bytes.NewReader(b)) 306 req.Header.Set("Accept", tc.auth.contentType) 307 resp, err := client.Do(req) 308 if err != nil { 309 if tests.EvalErrWithLog(t, errors.ErrGeneric.WithArgs("failed auth request", err), tc.want, tc.shouldErr, tc.err, msgs) { 310 return 311 } 312 } 313 respBody, err := ioutil.ReadAll(resp.Body) 314 resp.Body.Close() 315 if err != nil { 316 if tests.EvalErrWithLog(t, errors.ErrGeneric.WithArgs("failed reading auth request body", err), tc.want, tc.shouldErr, tc.err, msgs) { 317 return 318 } 319 } 320 if bytes.Contains(respBody, []byte(`"error":`)) { 321 if tests.EvalErrWithLog(t, errors.ErrGeneric.WithArgs("failed authentication request", respBody), tc.want, tc.shouldErr, tc.err, msgs) { 322 return 323 } 324 } 325 default: 326 // The use of url.Values is equivalent to using 327 // `strings.NewReader("username=webadmin&password=password123&realm=local")` 328 params := url.Values{} 329 params.Set("username", tc.auth.username) 330 params.Set("password", tc.auth.password) 331 params.Set("realm", tc.auth.realm) 332 req, err := http.NewRequest("POST", ts.URL+tc.auth.endpoint, strings.NewReader(params.Encode())) 333 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 334 req.Header.Set("Content-Length", strconv.Itoa(len(params.Encode()))) 335 resp, err := client.Do(req) 336 if err != nil { 337 t.Fatalf("failed authentication request: %v", err) 338 } 339 bearer := resp.Header.Get("Authorization") 340 if !strings.HasPrefix(bearer, "Bearer") { 341 t.Fatalf("failed authentication request: bearer header not found") 342 } 343 msgs = append(msgs, fmt.Sprintf("auth response: %s", bearer)) 344 } 345 } 346 347 // Process HTTP Requests. 348 for i, r := range tc.requests { 349 var pr string 350 if len(tc.requests) > 1 { 351 pr = fmt.Sprintf("req%02d_", i+1) 352 } 353 req, err := http.NewRequest(r.method, ts.URL+r.path, nil) 354 if err != nil { 355 got[pr+"error"] = err 356 continue 357 } 358 359 msgs = append(msgs, fmt.Sprintf("HTTP %s %s", r.method, ts.URL+r.path)) 360 if len(r.headers) > 0 { 361 for k, v := range r.headers { 362 req.Header.Add(k, v) 363 } 364 } 365 if len(r.query) > 0 { 366 q := req.URL.Query() 367 for k, v := range r.query { 368 q.Set(k, v) 369 } 370 req.URL.RawQuery = q.Encode() 371 } 372 373 var body []byte 374 resp, err := client.Do(req) 375 if err != nil { 376 got[pr+"error"] = fmt.Sprintf("failed request: %v", err) 377 } else { 378 body, err = ioutil.ReadAll(resp.Body) 379 resp.Body.Close() 380 if err != nil { 381 got[pr+"error"] = fmt.Sprintf("failed reading response: %v", err) 382 } 383 384 got[pr+"status_code"] = resp.StatusCode 385 got[pr+"content_type"] = resp.Header.Get("Content-Type") 386 switch resp.Header.Get("Content-Type") { 387 case "image/png": 388 default: 389 msgs = append(msgs, fmt.Sprintf("response body: %s", body)) 390 } 391 switch { 392 case bytes.HasPrefix(body, []byte(`{`)): 393 switch { 394 case bytes.Contains(body, []byte(`"error":`)): 395 b := &AccessDeniedResponse{} 396 json.Unmarshal(body, b) 397 got[pr+"message"] = b.Message 398 } 399 } 400 } 401 } 402 tests.EvalObjectsWithLog(t, "response", tc.want, got, msgs) 403 }) 404 } 405 }