github.com/volatiletech/authboss@v2.4.1+incompatible/oauth2/oauth2_test.go (about)

     1  package oauth2
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  	"net/http/httptest"
     7  	"net/url"
     8  	"strings"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/volatiletech/authboss"
    13  	"github.com/volatiletech/authboss/mocks"
    14  	"golang.org/x/oauth2"
    15  	"golang.org/x/oauth2/facebook"
    16  	"golang.org/x/oauth2/google"
    17  )
    18  
    19  func init() {
    20  	exchanger = func(_ *oauth2.Config, _ context.Context, _ string, _ ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
    21  		return testToken, nil
    22  	}
    23  }
    24  
    25  var testProviders = map[string]authboss.OAuth2Provider{
    26  	"google": {
    27  		OAuth2Config: &oauth2.Config{
    28  			ClientID:     `jazz`,
    29  			ClientSecret: `hands`,
    30  			Scopes:       []string{`profile`, `email`},
    31  			Endpoint:     google.Endpoint,
    32  			// This is typically set by Init() but some tests rely on it's existence
    33  			RedirectURL: "https://www.example.com/auth/oauth2/callback/google",
    34  		},
    35  		FindUserDetails:  GoogleUserDetails,
    36  		AdditionalParams: url.Values{"include_requested_scopes": []string{"true"}},
    37  	},
    38  	"facebook": {
    39  		OAuth2Config: &oauth2.Config{
    40  			ClientID:     `jazz`,
    41  			ClientSecret: `hands`,
    42  			Scopes:       []string{`email`},
    43  			Endpoint:     facebook.Endpoint,
    44  			// This is typically set by Init() but some tests rely on it's existence
    45  			RedirectURL: "https://www.example.com/auth/oauth2/callback/facebook",
    46  		},
    47  		FindUserDetails: FacebookUserDetails,
    48  	},
    49  }
    50  
    51  var testToken = &oauth2.Token{
    52  	AccessToken:  "token",
    53  	TokenType:    "Bearer",
    54  	RefreshToken: "refresh",
    55  	Expiry:       time.Now().AddDate(0, 0, 1),
    56  }
    57  
    58  func TestInit(t *testing.T) {
    59  	// No t.Parallel() since the cfg.RedirectURL is set in Init()
    60  
    61  	ab := authboss.New()
    62  	oauth := &OAuth2{}
    63  
    64  	router := &mocks.Router{}
    65  	ab.Config.Modules.OAuth2Providers = testProviders
    66  	ab.Config.Core.Router = router
    67  	ab.Config.Core.ErrorHandler = &mocks.ErrorHandler{}
    68  
    69  	ab.Config.Paths.Mount = "/auth"
    70  	ab.Config.Paths.RootURL = "https://www.example.com"
    71  
    72  	if err := oauth.Init(ab); err != nil {
    73  		t.Fatal(err)
    74  	}
    75  
    76  	gets := []string{
    77  		"/oauth2/facebook", "/oauth2/callback/facebook",
    78  		"/oauth2/google", "/oauth2/callback/google",
    79  	}
    80  	if err := router.HasGets(gets...); err != nil {
    81  		t.Error(err)
    82  	}
    83  }
    84  
    85  type testHarness struct {
    86  	oauth *OAuth2
    87  	ab    *authboss.Authboss
    88  
    89  	redirector *mocks.Redirector
    90  	session    *mocks.ClientStateRW
    91  	storer     *mocks.ServerStorer
    92  }
    93  
    94  func testSetup() *testHarness {
    95  	harness := &testHarness{}
    96  
    97  	harness.ab = authboss.New()
    98  	harness.redirector = &mocks.Redirector{}
    99  	harness.session = mocks.NewClientRW()
   100  	harness.storer = mocks.NewServerStorer()
   101  
   102  	harness.ab.Modules.OAuth2Providers = testProviders
   103  
   104  	harness.ab.Paths.OAuth2LoginOK = "/auth/oauth2/ok"
   105  	harness.ab.Paths.OAuth2LoginNotOK = "/auth/oauth2/not/ok"
   106  
   107  	harness.ab.Config.Core.Logger = mocks.Logger{}
   108  	harness.ab.Config.Core.Redirector = harness.redirector
   109  	harness.ab.Config.Storage.SessionState = harness.session
   110  	harness.ab.Config.Storage.Server = harness.storer
   111  
   112  	harness.oauth = &OAuth2{harness.ab}
   113  
   114  	return harness
   115  }
   116  
   117  func TestStart(t *testing.T) {
   118  	t.Parallel()
   119  
   120  	h := testSetup()
   121  
   122  	rec := httptest.NewRecorder()
   123  	w := h.ab.NewResponse(rec)
   124  	r := httptest.NewRequest("GET", "/oauth2/google?cake=yes&death=no", nil)
   125  
   126  	if err := h.oauth.Start(w, r); err != nil {
   127  		t.Error(err)
   128  	}
   129  
   130  	if h.redirector.Options.Code != http.StatusTemporaryRedirect {
   131  		t.Error("code was wrong:", h.redirector.Options.Code)
   132  	}
   133  
   134  	redirectPathUrl, err := url.Parse(h.redirector.Options.RedirectPath)
   135  	if err != nil {
   136  		t.Fatal(err)
   137  	}
   138  	query := redirectPathUrl.Query()
   139  	if state := query.Get("state"); len(state) == 0 {
   140  		t.Error("our nonce should have been here")
   141  	}
   142  	if callback := query.Get("redirect_uri"); callback != "https://www.example.com/auth/oauth2/callback/google" {
   143  		t.Error("callback was wrong:", callback)
   144  	}
   145  	if clientID := query.Get("client_id"); clientID != "jazz" {
   146  		t.Error("clientID was wrong:", clientID)
   147  	}
   148  	if redirectPathUrl.Host != "accounts.google.com" {
   149  		t.Error("host was wrong:", redirectPathUrl.Host)
   150  	}
   151  
   152  	if h.session.ClientValues[authboss.SessionOAuth2State] != query.Get("state") {
   153  		t.Error("the state should have been saved in the session")
   154  	}
   155  	if v := h.session.ClientValues[authboss.SessionOAuth2Params]; v != `{"cake":"yes","death":"no"}` {
   156  		t.Error("oauth2 session params are wrong:", v)
   157  	}
   158  }
   159  
   160  func TestStartBadProvider(t *testing.T) {
   161  	t.Parallel()
   162  
   163  	h := testSetup()
   164  
   165  	rec := httptest.NewRecorder()
   166  	w := h.ab.NewResponse(rec)
   167  	r := httptest.NewRequest("GET", "/oauth2/test", nil)
   168  
   169  	err := h.oauth.Start(w, r)
   170  	if e := err.Error(); !strings.Contains(e, `provider "test" not found`) {
   171  		t.Error("it should have errored:", e)
   172  	}
   173  }
   174  
   175  func TestEnd(t *testing.T) {
   176  	t.Parallel()
   177  
   178  	h := testSetup()
   179  
   180  	rec := httptest.NewRecorder()
   181  	w := h.ab.NewResponse(rec)
   182  
   183  	h.session.ClientValues[authboss.SessionOAuth2State] = "state"
   184  	r, err := h.ab.LoadClientState(w, httptest.NewRequest("GET", "/oauth2/callback/google?state=state", nil))
   185  	if err != nil {
   186  		t.Fatal(err)
   187  	}
   188  
   189  	if err := h.oauth.End(w, r); err != nil {
   190  		t.Error(err)
   191  	}
   192  
   193  	w.WriteHeader(http.StatusOK) // Flush headers
   194  
   195  	opts := h.redirector.Options
   196  	if opts.Code != http.StatusTemporaryRedirect {
   197  		t.Error("it should have redirected")
   198  	}
   199  	if opts.RedirectPath != "/auth/oauth2/ok" {
   200  		t.Error("redir path was wrong:", opts.RedirectPath)
   201  	}
   202  	if s := h.session.ClientValues[authboss.SessionKey]; s != "oauth2;;google;;id" {
   203  		t.Error("session id should have been set:", s)
   204  	}
   205  }
   206  
   207  func TestEndBadProvider(t *testing.T) {
   208  	t.Parallel()
   209  
   210  	h := testSetup()
   211  
   212  	rec := httptest.NewRecorder()
   213  	w := h.ab.NewResponse(rec)
   214  	r := httptest.NewRequest("GET", "/oauth2/callback/test", nil)
   215  
   216  	err := h.oauth.End(w, r)
   217  	if e := err.Error(); !strings.Contains(e, `provider "test" not found`) {
   218  		t.Error("it should have errored:", e)
   219  	}
   220  }
   221  
   222  func TestEndBadState(t *testing.T) {
   223  	t.Parallel()
   224  
   225  	h := testSetup()
   226  
   227  	rec := httptest.NewRecorder()
   228  	w := h.ab.NewResponse(rec)
   229  	r := httptest.NewRequest("GET", "/oauth2/callback/google", nil)
   230  
   231  	err := h.oauth.End(w, r)
   232  	if e := err.Error(); !strings.Contains(e, `oauth2 endpoint hit without session state`) {
   233  		t.Error("it should have errored:", e)
   234  	}
   235  
   236  	h.session.ClientValues[authboss.SessionOAuth2State] = "state"
   237  	r, err = h.ab.LoadClientState(w, httptest.NewRequest("GET", "/oauth2/callback/google?state=x", nil))
   238  	if err != nil {
   239  		t.Fatal(err)
   240  	}
   241  	if err := h.oauth.End(w, r); err != errOAuthStateValidation {
   242  		t.Error("error was wrong:", err)
   243  	}
   244  }
   245  
   246  func TestEndErrors(t *testing.T) {
   247  	t.Parallel()
   248  
   249  	h := testSetup()
   250  
   251  	rec := httptest.NewRecorder()
   252  	w := h.ab.NewResponse(rec)
   253  
   254  	h.session.ClientValues[authboss.SessionOAuth2State] = "state"
   255  	r, err := h.ab.LoadClientState(w, httptest.NewRequest("GET", "/oauth2/callback/google?state=state&error=badtimes&error_reason=reason", nil))
   256  	if err != nil {
   257  		t.Fatal(err)
   258  	}
   259  
   260  	if err := h.oauth.End(w, r); err != nil {
   261  		t.Error(err)
   262  	}
   263  
   264  	opts := h.redirector.Options
   265  	if opts.Code != http.StatusTemporaryRedirect {
   266  		t.Error("code was wrong:", opts.Code)
   267  	}
   268  	if opts.RedirectPath != "/auth/oauth2/not/ok" {
   269  		t.Error("path was wrong:", opts.RedirectPath)
   270  	}
   271  }
   272  
   273  func TestEndHandling(t *testing.T) {
   274  	t.Parallel()
   275  
   276  	t.Run("AfterOAuth2Fail", func(t *testing.T) {
   277  		h := testSetup()
   278  
   279  		rec := httptest.NewRecorder()
   280  		w := h.ab.NewResponse(rec)
   281  
   282  		h.session.ClientValues[authboss.SessionOAuth2State] = "state"
   283  		r, err := h.ab.LoadClientState(w, httptest.NewRequest("GET", "/oauth2/callback/google?state=state&error=badtimes&error_reason=reason", nil))
   284  		if err != nil {
   285  			t.Fatal(err)
   286  		}
   287  
   288  		called := false
   289  		h.ab.Events.After(authboss.EventOAuth2Fail, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
   290  			called = true
   291  			return true, nil
   292  		})
   293  
   294  		if err := h.oauth.End(w, r); err != nil {
   295  			t.Error(err)
   296  		}
   297  
   298  		if !called {
   299  			t.Error("it should have been called")
   300  		}
   301  		if h.redirector.Options.Code != 0 {
   302  			t.Error("it should not have tried to redirect")
   303  		}
   304  	})
   305  	t.Run("BeforeOAuth2", func(t *testing.T) {
   306  		h := testSetup()
   307  
   308  		rec := httptest.NewRecorder()
   309  		w := h.ab.NewResponse(rec)
   310  
   311  		h.session.ClientValues[authboss.SessionOAuth2State] = "state"
   312  		r, err := h.ab.LoadClientState(w, httptest.NewRequest("GET", "/oauth2/callback/google?state=state", nil))
   313  		if err != nil {
   314  			t.Fatal(err)
   315  		}
   316  
   317  		called := false
   318  		h.ab.Events.Before(authboss.EventOAuth2, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
   319  			called = true
   320  			return true, nil
   321  		})
   322  
   323  		if err := h.oauth.End(w, r); err != nil {
   324  			t.Error(err)
   325  		}
   326  
   327  		w.WriteHeader(http.StatusOK) // Flush headers
   328  
   329  		if !called {
   330  			t.Error("it should have been called")
   331  		}
   332  		if h.redirector.Options.Code != 0 {
   333  			t.Error("it should not have tried to redirect")
   334  		}
   335  		if len(h.session.ClientValues[authboss.SessionKey]) != 0 {
   336  			t.Error("should have not logged the user in")
   337  		}
   338  	})
   339  
   340  	t.Run("AfterOAuth2", func(t *testing.T) {
   341  		h := testSetup()
   342  
   343  		rec := httptest.NewRecorder()
   344  		w := h.ab.NewResponse(rec)
   345  
   346  		h.session.ClientValues[authboss.SessionOAuth2State] = "state"
   347  		r, err := h.ab.LoadClientState(w, httptest.NewRequest("GET", "/oauth2/callback/google?state=state", nil))
   348  		if err != nil {
   349  			t.Fatal(err)
   350  		}
   351  
   352  		called := false
   353  		h.ab.Events.After(authboss.EventOAuth2, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
   354  			called = true
   355  			return true, nil
   356  		})
   357  
   358  		if err := h.oauth.End(w, r); err != nil {
   359  			t.Error(err)
   360  		}
   361  
   362  		w.WriteHeader(http.StatusOK) // Flush headers
   363  
   364  		if !called {
   365  			t.Error("it should have been called")
   366  		}
   367  		if h.redirector.Options.Code != 0 {
   368  			t.Error("it should not have tried to redirect")
   369  		}
   370  		if s := h.session.ClientValues[authboss.SessionKey]; s != "oauth2;;google;;id" {
   371  			t.Error("session id should have been set:", s)
   372  		}
   373  	})
   374  }