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  }