auth

Paddy 2014-11-18 Parent:4ae226929e92 Child:a9936cf794ba

77:d43c3fbf00f3 Go to Latest

auth/oauth2_test.go

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..

History
1 package auth
3 import (
4 "bytes"
5 "html/template"
6 "io/ioutil"
7 "net/http"
8 "net/http/httptest"
9 "net/url"
10 "testing"
11 "time"
13 "code.secondbit.org/uuid"
14 )
16 const (
17 scopeSet = 1 << iota
18 stateSet
19 uriSet
20 )
22 func stripParam(param string, u *url.URL) {
23 q := u.Query()
24 q.Del(param)
25 u.RawQuery = q.Encode()
26 }
28 func TestGetGrantCodeSuccess(t *testing.T) {
29 t.Parallel()
30 store := NewMemstore()
31 testContext := Context{
32 template: template.Must(template.New(getGrantTemplateName).Parse("Get auth grant")),
33 clients: store,
34 grants: store,
35 profiles: store,
36 tokens: store,
37 sessions: store,
38 }
39 client := Client{
40 ID: uuid.NewID(),
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",
46 Type: "public",
47 }
48 uri, err := url.Parse("https://test.secondbit.org/redirect")
49 if err != nil {
50 t.Fatal("Can't parse URL:", err)
51 }
52 endpoint := Endpoint{
53 ID: uuid.NewID(),
54 ClientID: client.ID,
55 URI: *uri,
56 Added: time.Now(),
57 }
58 err = testContext.SaveClient(client)
59 if err != nil {
60 t.Fatal("Can't store client:", err)
61 }
62 err = testContext.AddEndpoint(client.ID, endpoint)
63 if err != nil {
64 t.Fatal("Can't store endpoint:", err)
65 }
66 session := Session{
67 ID: "testsession",
68 Active: true,
69 }
70 err = testContext.CreateSession(session)
71 if err != nil {
72 t.Fatal("Can't store session:", err)
73 }
74 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
75 if err != nil {
76 t.Fatal("Can't build request:", err)
77 }
78 cookie := &http.Cookie{
79 Name: authCookieName,
80 Value: session.ID,
81 }
82 req.AddCookie(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())
89 if i&uriSet != 0 {
90 params.Set("redirect_uri", endpoint.URI.String())
91 }
92 if i&scopeSet != 0 {
93 params.Set("scope", "testscope")
94 }
95 if i&stateSet != 0 {
96 params.Set("state", "my super secure state string")
97 }
98 req.URL.RawQuery = params.Encode()
99 req.Method = "GET"
100 req.Body = nil
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())
105 }
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())
108 }
109 req.Method = "POST"
110 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
111 w = httptest.NewRecorder()
112 data := url.Values{}
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())
119 }
120 redirectedTo := w.Header().Get("Location")
121 red, err := url.Parse(redirectedTo)
122 if err != nil {
123 t.Fatalf(`Being redirected to a non-URL "%s" threw error "%s" for "%s"\n`, redirectedTo, err, req.URL.String())
124 }
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())
128 }
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())
131 }
132 err = testContext.DeleteGrant(red.Query().Get("code"))
133 if err != nil {
134 t.Log(`Unexpected error "%s" deleting grant "%s" for %s`, err, red.Query().Get("code"), req.URL.String())
135 }
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())
139 }
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())
143 }
144 }
145 }
147 func TestGetGrantCodeInvalidClient(t *testing.T) {
148 t.Parallel()
149 store := NewMemstore()
150 testContext := Context{
151 template: template.Must(template.New(getGrantTemplateName).Parse("{{ .error }}")),
152 clients: store,
153 grants: store,
154 profiles: store,
155 tokens: store,
156 sessions: store,
157 }
158 client := Client{
159 ID: uuid.NewID(),
160 Secret: "super secret!",
161 OwnerID: uuid.NewID(),
162 Name: "My test client",
163 Type: "public",
164 }
165 err := testContext.SaveClient(client)
166 if err != nil {
167 t.Fatal("Can't store client:", err)
168 }
169 session := Session{
170 ID: "testsession",
171 Active: true,
172 }
173 err = testContext.CreateSession(session)
174 if err != nil {
175 t.Fatal("Can't store session:", err)
176 }
177 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
178 if err != nil {
179 t.Fatal("Can't build request:", err)
180 }
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,
188 Value: session.ID,
189 }
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)
194 }
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())
197 }
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)
204 }
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())
207 }
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)
214 }
215 if w.Body.String() != "The specified Client couldn&rsquo;t be found." {
216 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The specified Client couldn&rsquo;t be found.", w.Body.String())
217 }
218 }
220 func TestGetGrantCodeInvalidURI(t *testing.T) {
221 t.Parallel()
222 store := NewMemstore()
223 testContext := Context{
224 template: template.Must(template.New(getGrantTemplateName).Parse("{{ .error }}")),
225 clients: store,
226 grants: store,
227 profiles: store,
228 tokens: store,
229 sessions: store,
230 }
231 client := Client{
232 ID: uuid.NewID(),
233 Secret: "super secret!",
234 OwnerID: uuid.NewID(),
235 Name: "My test client",
236 Type: "public",
237 }
238 uri, err := url.Parse("https://test.secondbit.org/redirect")
239 if err != nil {
240 t.Fatal("Can't parse URL:", err)
241 }
242 err = testContext.SaveClient(client)
243 if err != nil {
244 t.Fatal("Can't store client:", err)
245 }
246 session := Session{
247 ID: "testsession",
248 Active: true,
249 }
250 err = testContext.CreateSession(session)
251 if err != nil {
252 t.Fatal("Can't store session:", err)
253 }
254 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
255 if err != nil {
256 t.Fatal("Can't build request:", err)
257 }
258 cookie := &http.Cookie{
259 Name: authCookieName,
260 Value: session.ID,
261 }
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)
271 }
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())
274 }
275 endpoint := Endpoint{
276 ID: uuid.NewID(),
277 ClientID: client.ID,
278 URI: *uri,
279 Added: time.Now(),
280 }
281 err = testContext.AddEndpoint(client.ID, endpoint)
282 if err != nil {
283 t.Fatal("Can't store endpoint:", err)
284 }
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)
291 }
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())
294 }
295 endpoint2 := Endpoint{
296 ID: uuid.NewID(),
297 ClientID: client.ID,
298 URI: *uri,
299 Added: time.Now(),
300 }
301 err = testContext.AddEndpoint(client.ID, endpoint2)
302 if err != nil {
303 t.Fatal("Can't store endpoint:", err)
304 }
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)
311 }
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())
314 }
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)
321 }
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())
324 }
325 }
327 func TestGetGrantCodeInvalidResponseType(t *testing.T) {
328 t.Parallel()
329 store := NewMemstore()
330 testContext := Context{
331 template: template.Must(template.New(getGrantTemplateName).Parse("{{ .error }}")),
332 clients: store,
333 grants: store,
334 profiles: store,
335 tokens: store,
336 sessions: store,
337 }
338 client := Client{
339 ID: uuid.NewID(),
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",
345 Type: "public",
346 }
347 uri, err := url.Parse("https://test.secondbit.org/redirect")
348 if err != nil {
349 t.Fatal("Can't parse URL:", err)
350 }
351 endpoint := Endpoint{
352 ID: uuid.NewID(),
353 ClientID: client.ID,
354 URI: *uri,
355 Added: time.Now(),
356 }
357 err = testContext.SaveClient(client)
358 if err != nil {
359 t.Fatal("Can't store client:", err)
360 }
361 err = testContext.AddEndpoint(client.ID, endpoint)
362 if err != nil {
363 t.Fatal("Can't store endpoint:", err)
364 }
365 session := Session{
366 ID: "testsession",
367 Active: true,
368 }
369 err = testContext.CreateSession(session)
370 if err != nil {
371 t.Fatal("Can't store session:", err)
372 }
373 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
374 if err != nil {
375 t.Fatal("Can't build request:", err)
376 }
377 cookie := &http.Cookie{
378 Name: authCookieName,
379 Value: session.ID,
380 }
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)
393 }
394 redirectedTo := w.Header().Get("Location")
395 red, err := url.Parse(redirectedTo)
396 if err != nil {
397 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
398 }
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"))
401 }
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"))
405 }
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())
409 }
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)
415 }
416 redirectedTo = w.Header().Get("Location")
417 red, err = url.Parse(redirectedTo)
418 if err != nil {
419 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
420 }
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"))
423 }
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"))
427 }
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())
431 }
432 }
434 func TestGetGrantCodeDenied(t *testing.T) {
435 t.Parallel()
436 store := NewMemstore()
437 testContext := Context{
438 template: template.Must(template.New(getGrantTemplateName).Parse("{{ .error }}")),
439 clients: store,
440 grants: store,
441 profiles: store,
442 tokens: store,
443 sessions: store,
444 }
445 client := Client{
446 ID: uuid.NewID(),
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",
452 Type: "public",
453 }
454 uri, err := url.Parse("https://test.secondbit.org/redirect")
455 if err != nil {
456 t.Fatal("Can't parse URL:", err)
457 }
458 endpoint := Endpoint{
459 ID: uuid.NewID(),
460 ClientID: client.ID,
461 URI: *uri,
462 Added: time.Now(),
463 }
464 err = testContext.SaveClient(client)
465 if err != nil {
466 t.Fatal("Can't store client:", err)
467 }
468 err = testContext.AddEndpoint(client.ID, endpoint)
469 if err != nil {
470 t.Fatal("Can't store endpoint:", err)
471 }
472 session := Session{
473 ID: "testsession",
474 Active: true,
475 }
476 err = testContext.CreateSession(session)
477 if err != nil {
478 t.Fatal("Can't store session:", err)
479 }
480 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
481 if err != nil {
482 t.Fatal("Can't build request:", err)
483 }
484 cookie := &http.Cookie{
485 Name: authCookieName,
486 Value: session.ID,
487 }
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")
495 data := url.Values{}
496 data.Set("grant", "denied")
497 req.URL.RawQuery = params.Encode()
498 req.Body = ioutil.NopCloser(bytes.NewBufferString(data.Encode()))
499 req.Method = "POST"
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)
504 }
505 redirectedTo := w.Header().Get("Location")
506 red, err := url.Parse(redirectedTo)
507 if err != nil {
508 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
509 }
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"))
512 }
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"))
516 }
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())
520 }
521 }
523 func TestGetBasicAuth(t *testing.T) {
524 tests := map[string]struct {
525 un string
526 pass string
527 err error
528 }{
529 "Basic dGVzdHVzZXI6cGFzc3dvcmQx": {"testuser", "password1", nil},
530 "": {"", "", ErrNoAuth},
531 "dGVzdHVzZXI6cGFzc3dvcmQx": {"", "", ErrInvalidAuthFormat},
532 "Basic _*&^##$@#$@&!!@": {"", "", ErrInvalidAuthFormat},
533 "Basic abcdefgh": {"", "", ErrInvalidAuthFormat},
534 "Basic dXNlcjo=": {"user", "", nil},
535 }
537 for header, test := range tests {
538 req, err := http.NewRequest("GET", "https://auth.secondbit.org", nil)
539 if err != nil {
540 t.Error("Unexpected error creating base request:", err)
541 }
542 req.Header.Set("Authorization", header)
543 un, pass, err := getBasicAuth(req)
544 if un != test.un {
545 t.Errorf(`Expected header "%s" to return username "%s", got "%s"`, header, test.un, un)
546 }
547 if pass != test.pass {
548 t.Errorf(`Expected header "%s" to return password "%s", got "%s"`, header, test.pass, pass)
549 }
550 if err != test.err {
551 t.Errorf(`Expected header "%s" to return error "%s", got "%s"`, header, test.err, err)
552 }
553 }
554 }