Write Session tests.
Add loginURI as a property to our Context, to keep track of where users should
be redirected to log in.
Implement the sessionStore in the memstore to let us test with Sessions.
Catch when the HTTP Basic Auth header doesn't include two parts, rather than
panicking. Return an ErrInvalidAuthFormat.
Clean up the error handling for checkCookie to be cleaner. Log unexpected errors
from request.Cookie.
Stop checking for cookie expiration times--those aren't sent to the server, so
we'll never get a valid session if we look for them.
Add a helper to build a login redirect URI--a URI the user can be redirected to
that has a URL-encoded URL to redirect the user back to after a successful
login.
Add a wrapper to wrap our Context into HTTP handlers.
Create a RegisterOAuth2 helper that adds the OAuth2 endpoints to a Gorilla/mux
router.
Redirect users to the login page when they have no session set or an invalid
session.
Return a server error when we can't check our cookie for whatever reason.
Log errors.
Add sessions to our OAuth2 tests so the tests stop failing--the session check
was interfering with them.
Add a test for our getBasicAuth helper to ensure that we're parsing basic auth
correctly.
Add an ErrSessionAlreadyExists error to be returned when a session has an ID
conflict.
Test that our memstore implementation of the sessionStore works as intended..
13 "code.secondbit.org/uuid"
22 func stripParam(param string, u *url.URL) {
25 u.RawQuery = q.Encode()
28 func TestGetGrantCodeSuccess(t *testing.T) {
30 store := NewMemstore()
31 testContext := Context{
32 template: template.Must(template.New(getGrantTemplateName).Parse("Get auth grant")),
41 Secret: "super secret!",
42 OwnerID: uuid.NewID(),
43 Name: "My test client",
44 Logo: "https://secondbit.org/logo.png",
45 Website: "https://secondbit.org",
48 uri, err := url.Parse("https://test.secondbit.org/redirect")
50 t.Fatal("Can't parse URL:", err)
58 err = testContext.SaveClient(client)
60 t.Fatal("Can't store client:", err)
62 err = testContext.AddEndpoint(client.ID, endpoint)
64 t.Fatal("Can't store endpoint:", err)
70 err = testContext.CreateSession(session)
72 t.Fatal("Can't store session:", err)
74 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
76 t.Fatal("Can't build request:", err)
78 cookie := &http.Cookie{
83 for i := 0; i < 1<<3; i++ {
84 w := httptest.NewRecorder()
85 params := url.Values{}
86 // see OAuth 2.0 spec, section 4.1.1
87 params.Set("response_type", "code")
88 params.Set("client_id", client.ID.String())
90 params.Set("redirect_uri", endpoint.URI.String())
93 params.Set("scope", "testscope")
96 params.Set("state", "my super secure state string")
98 req.URL.RawQuery = params.Encode()
101 req.Header.Del("Content-Type")
102 GetGrantHandler(w, req, testContext)
103 if w.Code != http.StatusOK {
104 t.Errorf("Expected status code to be %d, got %d for %s", http.StatusOK, w.Code, req.URL.String())
106 if w.Body.String() != "Get auth grant" {
107 t.Errorf("Expected body to be `%s`, got `%s` for %s", "Get auth grant", w.Body.String(), req.URL.String())
110 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
111 w = httptest.NewRecorder()
113 data.Set("grant", "approved")
114 body := bytes.NewBufferString(data.Encode())
115 req.Body = ioutil.NopCloser(body)
116 GetGrantHandler(w, req, testContext)
117 if w.Code != http.StatusFound {
118 t.Errorf("Expected status code to be %d, got %d for %s", http.StatusFound, w.Code, req.URL.String())
120 redirectedTo := w.Header().Get("Location")
121 red, err := url.Parse(redirectedTo)
123 t.Fatalf(`Being redirected to a non-URL "%s" threw error "%s" for "%s"\n`, redirectedTo, err, req.URL.String())
125 t.Log("Redirected to", redirectedTo)
126 if red.Query().Get("code") == "" {
127 t.Fatalf(`Expected code param in redirect URL to be set, but it wasn't for %s`, req.URL.String())
129 if _, err := testContext.GetGrant(red.Query().Get("code")); err != nil {
130 t.Fatalf(`Unexpected error "%s: retrieving the grant "%s" supplied in the redirect URL for %s`, err, red.Query().Get("code"), req.URL.String())
132 err = testContext.DeleteGrant(red.Query().Get("code"))
134 t.Log(`Unexpected error "%s" deleting grant "%s" for %s`, err, red.Query().Get("code"), req.URL.String())
136 stripParam("code", red)
137 if red.Query().Get("state") != params.Get("state") {
138 t.Errorf(`Expected state param in redirect URL to be "%s", got "%s" for %s`, params.Get("state"), red.Query().Get("state"), req.URL.String())
140 stripParam("state", red)
141 if red.String() != endpoint.URI.String() {
142 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI.String(), red.String())
147 func TestGetGrantCodeInvalidClient(t *testing.T) {
149 store := NewMemstore()
150 testContext := Context{
151 template: template.Must(template.New(getGrantTemplateName).Parse("{{ .error }}")),
160 Secret: "super secret!",
161 OwnerID: uuid.NewID(),
162 Name: "My test client",
165 err := testContext.SaveClient(client)
167 t.Fatal("Can't store client:", err)
173 err = testContext.CreateSession(session)
175 t.Fatal("Can't store session:", err)
177 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
179 t.Fatal("Can't build request:", err)
181 w := httptest.NewRecorder()
182 params := url.Values{}
183 params.Set("response_type", "code")
184 params.Set("redirect_uri", "https://test.secondbit.org/")
185 req.URL.RawQuery = params.Encode()
186 cookie := &http.Cookie{
187 Name: authCookieName,
190 req.AddCookie(cookie)
191 GetGrantHandler(w, req, testContext)
192 if w.Code != http.StatusBadRequest {
193 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
195 if w.Body.String() != "Client ID must be specified in the request." {
196 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "Client ID must be specified in the request.", w.Body.String())
198 w = httptest.NewRecorder()
199 params.Set("client_id", "Not an ID")
200 req.URL.RawQuery = params.Encode()
201 GetGrantHandler(w, req, testContext)
202 if w.Code != http.StatusBadRequest {
203 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
205 if w.Body.String() != "client_id is not a valid Client ID." {
206 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "client_id is not a valid Client ID.", w.Body.String())
208 w = httptest.NewRecorder()
209 params.Set("client_id", uuid.NewID().String())
210 req.URL.RawQuery = params.Encode()
211 GetGrantHandler(w, req, testContext)
212 if w.Code != http.StatusBadRequest {
213 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
215 if w.Body.String() != "The specified Client couldn’t be found." {
216 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The specified Client couldn’t be found.", w.Body.String())
220 func TestGetGrantCodeInvalidURI(t *testing.T) {
222 store := NewMemstore()
223 testContext := Context{
224 template: template.Must(template.New(getGrantTemplateName).Parse("{{ .error }}")),
233 Secret: "super secret!",
234 OwnerID: uuid.NewID(),
235 Name: "My test client",
238 uri, err := url.Parse("https://test.secondbit.org/redirect")
240 t.Fatal("Can't parse URL:", err)
242 err = testContext.SaveClient(client)
244 t.Fatal("Can't store client:", err)
250 err = testContext.CreateSession(session)
252 t.Fatal("Can't store session:", err)
254 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
256 t.Fatal("Can't build request:", err)
258 cookie := &http.Cookie{
259 Name: authCookieName,
262 req.AddCookie(cookie)
263 w := httptest.NewRecorder()
264 params := url.Values{}
265 params.Set("response_type", "code")
266 params.Set("client_id", client.ID.String())
267 req.URL.RawQuery = params.Encode()
268 GetGrantHandler(w, req, testContext)
269 if w.Code != http.StatusBadRequest {
270 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
272 if w.Body.String() != "The redirect_uri specified is not valid." {
273 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
275 endpoint := Endpoint{
281 err = testContext.AddEndpoint(client.ID, endpoint)
283 t.Fatal("Can't store endpoint:", err)
285 w = httptest.NewRecorder()
286 params.Set("redirect_uri", "https://test.secondbit.org/wrong")
287 req.URL.RawQuery = params.Encode()
288 GetGrantHandler(w, req, testContext)
289 if w.Code != http.StatusBadRequest {
290 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
292 if w.Body.String() != "The redirect_uri specified is not valid." {
293 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
295 endpoint2 := Endpoint{
301 err = testContext.AddEndpoint(client.ID, endpoint2)
303 t.Fatal("Can't store endpoint:", err)
305 w = httptest.NewRecorder()
306 params.Set("redirect_uri", "")
307 req.URL.RawQuery = params.Encode()
308 GetGrantHandler(w, req, testContext)
309 if w.Code != http.StatusBadRequest {
310 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
312 if w.Body.String() != "The redirect_uri specified is not valid." {
313 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
315 w = httptest.NewRecorder()
316 params.Set("redirect_uri", "://not a URL")
317 req.URL.RawQuery = params.Encode()
318 GetGrantHandler(w, req, testContext)
319 if w.Code != http.StatusBadRequest {
320 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
322 if w.Body.String() != "The redirect_uri specified is not valid." {
323 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
327 func TestGetGrantCodeInvalidResponseType(t *testing.T) {
329 store := NewMemstore()
330 testContext := Context{
331 template: template.Must(template.New(getGrantTemplateName).Parse("{{ .error }}")),
340 Secret: "super secret!",
341 OwnerID: uuid.NewID(),
342 Name: "My test client",
343 Logo: "https://secondbit.org/logo.png",
344 Website: "https://secondbit.org",
347 uri, err := url.Parse("https://test.secondbit.org/redirect")
349 t.Fatal("Can't parse URL:", err)
351 endpoint := Endpoint{
357 err = testContext.SaveClient(client)
359 t.Fatal("Can't store client:", err)
361 err = testContext.AddEndpoint(client.ID, endpoint)
363 t.Fatal("Can't store endpoint:", err)
369 err = testContext.CreateSession(session)
371 t.Fatal("Can't store session:", err)
373 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
375 t.Fatal("Can't build request:", err)
377 cookie := &http.Cookie{
378 Name: authCookieName,
381 req.AddCookie(cookie)
382 params := url.Values{}
383 params.Set("response_type", "totally not code")
384 params.Set("client_id", client.ID.String())
385 params.Set("redirect_uri", endpoint.URI.String())
386 params.Set("scope", "testscope")
387 params.Set("state", "my super secure state string")
388 req.URL.RawQuery = params.Encode()
389 w := httptest.NewRecorder()
390 GetGrantHandler(w, req, testContext)
391 if w.Code != http.StatusFound {
392 t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
394 redirectedTo := w.Header().Get("Location")
395 red, err := url.Parse(redirectedTo)
397 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
399 if red.Query().Get("error") != "invalid_request" {
400 t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "invalid_request", red.Query().Get("error"))
402 stripParam("error", red)
403 if red.Query().Get("state") != params.Get("state") {
404 t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
406 stripParam("state", red)
407 if red.String() != endpoint.URI.String() {
408 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI.String(), red.String())
410 stripParam("response_type", req.URL)
411 w = httptest.NewRecorder()
412 GetGrantHandler(w, req, testContext)
413 if w.Code != http.StatusFound {
414 t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
416 redirectedTo = w.Header().Get("Location")
417 red, err = url.Parse(redirectedTo)
419 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
421 if red.Query().Get("error") != "invalid_request" {
422 t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "invalid_request", red.Query().Get("error"))
424 stripParam("error", red)
425 if red.Query().Get("state") != params.Get("state") {
426 t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
428 stripParam("state", red)
429 if red.String() != endpoint.URI.String() {
430 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI.String(), red.String())
434 func TestGetGrantCodeDenied(t *testing.T) {
436 store := NewMemstore()
437 testContext := Context{
438 template: template.Must(template.New(getGrantTemplateName).Parse("{{ .error }}")),
447 Secret: "super secret!",
448 OwnerID: uuid.NewID(),
449 Name: "My test client",
450 Logo: "https://secondbit.org/logo.png",
451 Website: "https://secondbit.org",
454 uri, err := url.Parse("https://test.secondbit.org/redirect")
456 t.Fatal("Can't parse URL:", err)
458 endpoint := Endpoint{
464 err = testContext.SaveClient(client)
466 t.Fatal("Can't store client:", err)
468 err = testContext.AddEndpoint(client.ID, endpoint)
470 t.Fatal("Can't store endpoint:", err)
476 err = testContext.CreateSession(session)
478 t.Fatal("Can't store session:", err)
480 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
482 t.Fatal("Can't build request:", err)
484 cookie := &http.Cookie{
485 Name: authCookieName,
488 req.AddCookie(cookie)
489 params := url.Values{}
490 params.Set("response_type", "code")
491 params.Set("client_id", client.ID.String())
492 params.Set("redirect_uri", endpoint.URI.String())
493 params.Set("scope", "testscope")
494 params.Set("state", "my super secure state string")
496 data.Set("grant", "denied")
497 req.URL.RawQuery = params.Encode()
498 req.Body = ioutil.NopCloser(bytes.NewBufferString(data.Encode()))
500 w := httptest.NewRecorder()
501 GetGrantHandler(w, req, testContext)
502 if w.Code != http.StatusFound {
503 t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
505 redirectedTo := w.Header().Get("Location")
506 red, err := url.Parse(redirectedTo)
508 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
510 if red.Query().Get("error") != "access_denied" {
511 t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "access_denied", red.Query().Get("error"))
513 stripParam("error", red)
514 if red.Query().Get("state") != params.Get("state") {
515 t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
517 stripParam("state", red)
518 if red.String() != endpoint.URI.String() {
519 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI.String(), red.String())
523 func TestGetBasicAuth(t *testing.T) {
524 tests := map[string]struct {
529 "Basic dGVzdHVzZXI6cGFzc3dvcmQx": {"testuser", "password1", nil},
530 "": {"", "", ErrNoAuth},
531 "dGVzdHVzZXI6cGFzc3dvcmQx": {"", "", ErrInvalidAuthFormat},
532 "Basic _*&^##$@#$@&!!@": {"", "", ErrInvalidAuthFormat},
533 "Basic abcdefgh": {"", "", ErrInvalidAuthFormat},
534 "Basic dXNlcjo=": {"user", "", nil},
537 for header, test := range tests {
538 req, err := http.NewRequest("GET", "https://auth.secondbit.org", nil)
540 t.Error("Unexpected error creating base request:", err)
542 req.Header.Set("Authorization", header)
543 un, pass, err := getBasicAuth(req)
545 t.Errorf(`Expected header "%s" to return username "%s", got "%s"`, header, test.un, un)
547 if pass != test.pass {
548 t.Errorf(`Expected header "%s" to return password "%s", got "%s"`, header, test.pass, pass)
551 t.Errorf(`Expected header "%s" to return error "%s", got "%s"`, header, test.err, err)