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 }