auth
auth/oauth2_test.go
Enable CSRF protection, add expiration to sessions. Sessions gain a CSRF token, which is passed as a parameter to the login page. The login page now checks for that CSRF token, and logs a CSRF attempt if the token does not match. I also added an expiration to sessions, so they don't last forever. Sessions should be pretty short--we just need to stay logged in for long enough to approve the OAuth request. Everything after that should be cookie based. Finally, I added a configuration parameter to control whether the session cookie should be set to Secure, requiring the use of HTTPS. For production use, this flag is a requirement, but it makes testing extremely difficult, so we need a way to disable it.
1 package auth
3 import (
4 "bytes"
5 "encoding/json"
6 "html/template"
7 "io/ioutil"
8 "net/http"
9 "net/http/httptest"
10 "net/url"
11 "testing"
12 "time"
14 "code.secondbit.org/uuid.hg"
15 )
17 const (
18 scopeSet = 1 << iota
19 stateSet
20 uriSet
21 )
23 func stripParam(param string, u *url.URL) {
24 q := u.Query()
25 q.Del(param)
26 u.RawQuery = q.Encode()
27 }
29 func TestGetAuthorizationCodeCodeSuccess(t *testing.T) {
30 t.Parallel()
31 store := NewMemstore()
32 testContext := Context{
33 template: template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ if not .error }}Get auth grant{{ else }}{{ .error }}{{ end }}")),
34 clients: store,
35 authCodes: store,
36 profiles: store,
37 tokens: store,
38 sessions: store,
39 }
40 client := Client{
41 ID: uuid.NewID(),
42 Secret: "super secret!",
43 OwnerID: uuid.NewID(),
44 Name: "My test client",
45 Logo: "https://secondbit.org/logo.png",
46 Website: "https://secondbit.org",
47 Type: "public",
48 }
49 endpoint := Endpoint{
50 ID: uuid.NewID(),
51 ClientID: client.ID,
52 URI: "https://test.secondbit.org/redirect",
53 Added: time.Now(),
54 }
55 err := testContext.SaveClient(client)
56 if err != nil {
57 t.Fatal("Can't store client:", err)
58 }
59 err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
60 if err != nil {
61 t.Fatal("Can't store endpoint:", err)
62 }
63 profile := Profile{
64 ID: uuid.NewID(),
65 }
66 err = testContext.SaveProfile(profile)
67 if err != nil {
68 t.Fatal("Can't store profile:", err)
69 }
70 session := Session{
71 ID: "testsession",
72 Active: true,
73 ProfileID: profile.ID,
74 CSRFToken: "nosurf",
75 Expires: time.Now().Add(time.Hour),
76 }
77 err = testContext.CreateSession(session)
78 if err != nil {
79 t.Fatal("Can't store session:", err)
80 }
81 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
82 if err != nil {
83 t.Fatal("Can't build request:", err)
84 }
85 cookie := &http.Cookie{
86 Name: authCookieName,
87 Value: session.ID,
88 }
89 req.AddCookie(cookie)
90 for i := 0; i < 1<<3; i++ {
91 w := httptest.NewRecorder()
92 params := url.Values{}
93 // see OAuth 2.0 spec, section 4.1.1
94 params.Set("response_type", "code")
95 params.Set("client_id", client.ID.String())
96 if i&uriSet != 0 {
97 params.Set("redirect_uri", endpoint.URI)
98 }
99 if i&scopeSet != 0 {
100 params.Set("scope", "testscope")
101 }
102 if i&stateSet != 0 {
103 params.Set("state", "my super secure state string")
104 }
105 req.URL.RawQuery = params.Encode()
106 req.Method = "GET"
107 req.Body = nil
108 req.Header.Del("Content-Type")
109 GetAuthorizationCodeHandler(w, req, testContext)
110 if w.Code != http.StatusOK {
111 t.Log("Response body:", w.Body.String())
112 t.Errorf("Expected status code to be %d, got %d for %s", http.StatusOK, w.Code, req.URL.String())
113 }
114 if w.Body.String() != "Get auth grant" {
115 t.Errorf("Expected body to be `%s`, got `%s` for %s", "Get auth grant", w.Body.String(), req.URL.String())
116 }
117 req.Method = "POST"
118 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
119 w = httptest.NewRecorder()
120 data := url.Values{}
121 data.Set("grant", "approved")
122 data.Set("csrftoken", session.CSRFToken)
123 body := bytes.NewBufferString(data.Encode())
124 req.Body = ioutil.NopCloser(body)
125 GetAuthorizationCodeHandler(w, req, testContext)
126 if w.Code != http.StatusFound {
127 t.Errorf("Expected status code to be %d, got %d for %s", http.StatusFound, w.Code, req.URL.String())
128 }
129 redirectedTo := w.Header().Get("Location")
130 red, err := url.Parse(redirectedTo)
131 if err != nil {
132 t.Fatalf(`Being redirected to a non-URL "%s" threw error "%s" for "%s"\n`, redirectedTo, err, req.URL.String())
133 }
134 t.Log("Redirected to", redirectedTo)
135 t.Log("Body", w.Body.String())
136 if red.Query().Get("code") == "" {
137 t.Fatalf(`Expected code param in redirect URL to be set, but it wasn't for %s`, req.URL.String())
138 }
139 if _, err := testContext.GetAuthorizationCode(red.Query().Get("code")); err != nil {
140 t.Fatalf(`Unexpected error "%s: retrieving the grant "%s" supplied in the redirect URL for %s`, err, red.Query().Get("code"), req.URL.String())
141 }
142 err = testContext.DeleteAuthorizationCode(red.Query().Get("code"))
143 if err != nil {
144 t.Logf(`Unexpected error "%s" deleting grant "%s" for %s`, err, red.Query().Get("code"), req.URL.String())
145 }
146 stripParam("code", red)
147 if red.Query().Get("state") != params.Get("state") {
148 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())
149 }
150 stripParam("state", red)
151 if red.String() != endpoint.URI {
152 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI, red.String())
153 }
154 }
155 }
157 func TestGetAuthorizationCodeCodeInvalidClient(t *testing.T) {
158 t.Parallel()
159 store := NewMemstore()
160 testContext := Context{
161 template: template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .error }}")),
162 clients: store,
163 authCodes: store,
164 profiles: store,
165 tokens: store,
166 sessions: store,
167 }
168 client := Client{
169 ID: uuid.NewID(),
170 Secret: "super secret!",
171 OwnerID: uuid.NewID(),
172 Name: "My test client",
173 Type: "public",
174 }
175 err := testContext.SaveClient(client)
176 if err != nil {
177 t.Fatal("Can't store client:", err)
178 }
179 session := Session{
180 ID: "testsession",
181 Active: true,
182 CSRFToken: "nosurf",
183 Expires: time.Now().Add(time.Hour),
184 }
185 err = testContext.CreateSession(session)
186 if err != nil {
187 t.Fatal("Can't store session:", err)
188 }
189 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
190 if err != nil {
191 t.Fatal("Can't build request:", err)
192 }
193 w := httptest.NewRecorder()
194 params := url.Values{}
195 params.Set("response_type", "code")
196 params.Set("redirect_uri", "https://test.secondbit.org/")
197 req.URL.RawQuery = params.Encode()
198 cookie := &http.Cookie{
199 Name: authCookieName,
200 Value: session.ID,
201 }
202 req.AddCookie(cookie)
203 GetAuthorizationCodeHandler(w, req, testContext)
204 if w.Code != http.StatusBadRequest {
205 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
206 }
207 if w.Body.String() != "Client ID must be specified in the request." {
208 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "Client ID must be specified in the request.", w.Body.String())
209 }
210 w = httptest.NewRecorder()
211 params.Set("client_id", "Not an ID")
212 req.URL.RawQuery = params.Encode()
213 GetAuthorizationCodeHandler(w, req, testContext)
214 if w.Code != http.StatusBadRequest {
215 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
216 }
217 if w.Body.String() != "client_id is not a valid Client ID." {
218 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "client_id is not a valid Client ID.", w.Body.String())
219 }
220 w = httptest.NewRecorder()
221 params.Set("client_id", uuid.NewID().String())
222 req.URL.RawQuery = params.Encode()
223 GetAuthorizationCodeHandler(w, req, testContext)
224 if w.Code != http.StatusBadRequest {
225 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
226 }
227 if w.Body.String() != "The specified Client couldn’t be found." {
228 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The specified Client couldn’t be found.", w.Body.String())
229 }
230 }
232 func TestGetAuthorizationCodeCodeInvalidURI(t *testing.T) {
233 t.Parallel()
234 store := NewMemstore()
235 testContext := Context{
236 template: template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .error }}")),
237 clients: store,
238 authCodes: store,
239 profiles: store,
240 tokens: store,
241 sessions: store,
242 }
243 client := Client{
244 ID: uuid.NewID(),
245 Secret: "super secret!",
246 OwnerID: uuid.NewID(),
247 Name: "My test client",
248 Type: "public",
249 }
250 err := testContext.SaveClient(client)
251 if err != nil {
252 t.Fatal("Can't store client:", err)
253 }
254 session := Session{
255 ID: "testsession",
256 Active: true,
257 CSRFToken: "nosurf",
258 Expires: time.Now().Add(time.Hour),
259 }
260 err = testContext.CreateSession(session)
261 if err != nil {
262 t.Fatal("Can't store session:", err)
263 }
264 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
265 if err != nil {
266 t.Fatal("Can't build request:", err)
267 }
268 cookie := &http.Cookie{
269 Name: authCookieName,
270 Value: session.ID,
271 }
272 req.AddCookie(cookie)
273 w := httptest.NewRecorder()
274 params := url.Values{}
275 params.Set("response_type", "code")
276 params.Set("client_id", client.ID.String())
277 req.URL.RawQuery = params.Encode()
278 GetAuthorizationCodeHandler(w, req, testContext)
279 if w.Code != http.StatusBadRequest {
280 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
281 }
282 if w.Body.String() != "The redirect_uri specified is not valid." {
283 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
284 }
285 endpoint := Endpoint{
286 ID: uuid.NewID(),
287 ClientID: client.ID,
288 URI: "https://test.secondbit.org/redirect",
289 Added: time.Now(),
290 }
291 err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
292 if err != nil {
293 t.Fatal("Can't store endpoint:", err)
294 }
295 w = httptest.NewRecorder()
296 params.Set("redirect_uri", "https://test.secondbit.org/wrong")
297 req.URL.RawQuery = params.Encode()
298 GetAuthorizationCodeHandler(w, req, testContext)
299 if w.Code != http.StatusBadRequest {
300 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
301 }
302 if w.Body.String() != "The redirect_uri specified is not valid." {
303 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
304 }
305 endpoint2 := Endpoint{
306 ID: uuid.NewID(),
307 ClientID: client.ID,
308 URI: "https://test.secondbit.org/redirect",
309 Added: time.Now(),
310 }
311 err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint2})
312 if err != nil {
313 t.Fatal("Can't store endpoint:", err)
314 }
315 w = httptest.NewRecorder()
316 params.Set("redirect_uri", "")
317 req.URL.RawQuery = params.Encode()
318 GetAuthorizationCodeHandler(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 w = httptest.NewRecorder()
326 params.Set("redirect_uri", "://not a URL")
327 req.URL.RawQuery = params.Encode()
328 GetAuthorizationCodeHandler(w, req, testContext)
329 if w.Code != http.StatusBadRequest {
330 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
331 }
332 if w.Body.String() != "The redirect_uri specified is not valid." {
333 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
334 }
335 // BUG(paddy): Need to test that setting redirect_uri to a non-URL redirect_uri returns the correct error.
336 }
338 func TestGetAuthorizationCodeCodeInvalidResponseType(t *testing.T) {
339 t.Parallel()
340 store := NewMemstore()
341 testContext := Context{
342 template: template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .error }}")),
343 clients: store,
344 authCodes: store,
345 profiles: store,
346 tokens: store,
347 sessions: store,
348 }
349 client := Client{
350 ID: uuid.NewID(),
351 Secret: "super secret!",
352 OwnerID: uuid.NewID(),
353 Name: "My test client",
354 Logo: "https://secondbit.org/logo.png",
355 Website: "https://secondbit.org",
356 Type: "public",
357 }
358 endpoint := Endpoint{
359 ID: uuid.NewID(),
360 ClientID: client.ID,
361 URI: "https://test.secondbit.org/redirect",
362 Added: time.Now(),
363 }
364 err := testContext.SaveClient(client)
365 if err != nil {
366 t.Fatal("Can't store client:", err)
367 }
368 err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
369 if err != nil {
370 t.Fatal("Can't store endpoint:", err)
371 }
372 session := Session{
373 ID: "testsession",
374 Active: true,
375 CSRFToken: "nosurf",
376 Expires: time.Now().Add(time.Hour),
377 }
378 err = testContext.CreateSession(session)
379 if err != nil {
380 t.Fatal("Can't store session:", err)
381 }
382 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
383 if err != nil {
384 t.Fatal("Can't build request:", err)
385 }
386 cookie := &http.Cookie{
387 Name: authCookieName,
388 Value: session.ID,
389 }
390 req.AddCookie(cookie)
391 params := url.Values{}
392 params.Set("response_type", "totally not code")
393 params.Set("client_id", client.ID.String())
394 params.Set("redirect_uri", endpoint.URI)
395 params.Set("scope", "testscope")
396 params.Set("state", "my super secure state string")
397 req.URL.RawQuery = params.Encode()
398 w := httptest.NewRecorder()
399 GetAuthorizationCodeHandler(w, req, testContext)
400 if w.Code != http.StatusFound {
401 t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
402 }
403 redirectedTo := w.Header().Get("Location")
404 red, err := url.Parse(redirectedTo)
405 if err != nil {
406 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
407 }
408 if red.Query().Get("error") != "invalid_request" {
409 t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "invalid_request", red.Query().Get("error"))
410 }
411 stripParam("error", red)
412 if red.Query().Get("state") != params.Get("state") {
413 t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
414 }
415 stripParam("state", red)
416 if red.String() != endpoint.URI {
417 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI, red.String())
418 }
419 stripParam("response_type", req.URL)
420 w = httptest.NewRecorder()
421 GetAuthorizationCodeHandler(w, req, testContext)
422 if w.Code != http.StatusFound {
423 t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
424 }
425 redirectedTo = w.Header().Get("Location")
426 red, err = url.Parse(redirectedTo)
427 if err != nil {
428 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
429 }
430 if red.Query().Get("error") != "invalid_request" {
431 t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "invalid_request", red.Query().Get("error"))
432 }
433 stripParam("error", red)
434 if red.Query().Get("state") != params.Get("state") {
435 t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
436 }
437 stripParam("state", red)
438 if red.String() != endpoint.URI {
439 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI, red.String())
440 }
441 }
443 func TestGetAuthorizationCodeCodeDenied(t *testing.T) {
444 t.Parallel()
445 store := NewMemstore()
446 testContext := Context{
447 template: template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .error }}")),
448 clients: store,
449 authCodes: store,
450 profiles: store,
451 tokens: store,
452 sessions: store,
453 }
454 client := Client{
455 ID: uuid.NewID(),
456 Secret: "super secret!",
457 OwnerID: uuid.NewID(),
458 Name: "My test client",
459 Logo: "https://secondbit.org/logo.png",
460 Website: "https://secondbit.org",
461 Type: "public",
462 }
463 endpoint := Endpoint{
464 ID: uuid.NewID(),
465 ClientID: client.ID,
466 URI: "https://test.secondbit.org/redirect",
467 Added: time.Now(),
468 }
469 err := testContext.SaveClient(client)
470 if err != nil {
471 t.Fatal("Can't store client:", err)
472 }
473 err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
474 if err != nil {
475 t.Fatal("Can't store endpoint:", err)
476 }
477 session := Session{
478 ID: "testsession",
479 Active: true,
480 CSRFToken: "nosurf",
481 Expires: time.Now().Add(time.Hour),
482 }
483 err = testContext.CreateSession(session)
484 if err != nil {
485 t.Fatal("Can't store session:", err)
486 }
487 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
488 if err != nil {
489 t.Fatal("Can't build request:", err)
490 }
491 cookie := &http.Cookie{
492 Name: authCookieName,
493 Value: session.ID,
494 }
495 req.AddCookie(cookie)
496 params := url.Values{}
497 params.Set("response_type", "code")
498 params.Set("client_id", client.ID.String())
499 params.Set("redirect_uri", endpoint.URI)
500 params.Set("scope", "testscope")
501 params.Set("state", "my super secure state string")
502 data := url.Values{}
503 data.Set("grant", "denied")
504 data.Set("csrftoken", session.CSRFToken)
505 req.URL.RawQuery = params.Encode()
506 req.Body = ioutil.NopCloser(bytes.NewBufferString(data.Encode()))
507 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
508 req.Method = "POST"
509 w := httptest.NewRecorder()
510 GetAuthorizationCodeHandler(w, req, testContext)
511 if w.Code != http.StatusFound {
512 t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
513 }
514 redirectedTo := w.Header().Get("Location")
515 red, err := url.Parse(redirectedTo)
516 if err != nil {
517 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
518 }
519 if red.Query().Get("error") != "access_denied" {
520 t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "access_denied", red.Query().Get("error"))
521 }
522 stripParam("error", red)
523 if red.Query().Get("state") != params.Get("state") {
524 t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
525 }
526 stripParam("state", red)
527 if red.String() != endpoint.URI {
528 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI, red.String())
529 }
530 }
532 func TestGetAuthorizationCodeCodeLoginRedirect(t *testing.T) {
533 t.Parallel()
534 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
535 if err != nil {
536 t.Fatal("Can't build request:", err)
537 }
538 w := httptest.NewRecorder()
539 GetAuthorizationCodeHandler(w, req, Context{template: template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .internal_error }}"))})
540 if w.Code != http.StatusInternalServerError {
541 t.Errorf("Expected status code to be %d, got %d", http.StatusInternalServerError, w.Code)
542 }
543 expectation := "Missing login URL."
544 if w.Body.String() != expectation {
545 t.Errorf(`Expected body to be "%s", got "%s"`, expectation, w.Body.String())
546 }
547 uri, err := url.Parse("https://client.secondbit.org/")
548 if err != nil {
549 t.Error("Unexpected error", err)
550 }
551 testContext := Context{
552 loginURI: uri,
553 }
554 w = httptest.NewRecorder()
555 GetAuthorizationCodeHandler(w, req, testContext)
556 if w.Code != http.StatusFound {
557 t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
558 }
559 redirectedTo := w.Header().Get("Location")
560 expectation = "https://client.secondbit.org/?from=https%3A%2F%2Ftest.auth.secondbit.org%2Foauth2%2Fgrant"
561 if redirectedTo != expectation {
562 t.Errorf("Expected to be redirected to '%s', was redirected to '%s'", expectation, redirectedTo)
563 }
564 }
566 // BUG(paddy): Need to test for implicit grant flow
568 func TestCheckCookie(t *testing.T) {
569 t.Parallel()
570 req, err := http.NewRequest("GET", "https://auth.secondbit.org", nil)
571 if err != nil {
572 t.Error("Unexpected error creating base request:", err)
573 }
574 store := NewMemstore()
575 testContext := Context{
576 sessions: store,
577 }
578 session, err := checkCookie(req, testContext)
579 if err != ErrNoSession {
580 t.Errorf("Expected ErrNoSession, got %s", err)
581 }
582 session = Session{
583 ID: "testsession",
584 Active: true,
585 CSRFToken: "nosurf",
586 Expires: time.Now().Add(time.Hour),
587 }
588 err = testContext.CreateSession(session)
589 if err != nil {
590 t.Error("Unexpected error persisting session:", err)
591 }
592 invalidSession := Session{
593 ID: "testsession2",
594 Active: false,
595 }
596 err = testContext.CreateSession(invalidSession)
597 if err != nil {
598 t.Error("Unexpected error persisting session:", err)
599 }
600 result, err := checkCookie(req, testContext)
601 if err != ErrNoSession {
602 t.Errorf("Expected ErrNoSession, got %s", err)
603 }
604 req.AddCookie(&http.Cookie{
605 Name: "wrongcookie",
606 Value: "wrong value",
607 })
608 result, err = checkCookie(req, testContext)
609 if err != ErrNoSession {
610 t.Error("Expected ErrNoSession, got", err)
611 }
612 req, err = http.NewRequest("GET", "https://auth.secondbit.org", nil)
613 if err != nil {
614 t.Error("Unexpected error creating base request:", err)
615 }
616 req.AddCookie(&http.Cookie{
617 Name: "Stillwrongcookie",
618 Value: session.ID,
619 })
620 result, err = checkCookie(req, testContext)
621 if err != ErrNoSession {
622 t.Error("Expected ErrNoSession, got", err)
623 }
624 req, err = http.NewRequest("GET", "https://auth.secondbit.org", nil)
625 if err != nil {
626 t.Error("Unexpected error creating base request:", err)
627 }
628 req.AddCookie(&http.Cookie{
629 Name: authCookieName,
630 Value: "wrong value",
631 })
632 result, err = checkCookie(req, testContext)
633 if err != ErrInvalidSession {
634 t.Error("Expected ErrInvalidSession, got", err)
635 }
636 req, err = http.NewRequest("GET", "https://auth.secondbit.org", nil)
637 if err != nil {
638 t.Error("Unexpected error creating base request:", err)
639 }
640 req.AddCookie(&http.Cookie{
641 Name: authCookieName,
642 Value: invalidSession.ID,
643 })
644 result, err = checkCookie(req, testContext)
645 if err != ErrInvalidSession {
646 t.Error("Expected ErrInvalidSession, got", err)
647 }
648 req, err = http.NewRequest("GET", "https://auth.secondbit.org", nil)
649 if err != nil {
650 t.Error("Unexpected error creating base request:", err)
651 }
652 req.AddCookie(&http.Cookie{
653 Name: authCookieName,
654 Value: session.ID,
655 })
656 result, err = checkCookie(req, testContext)
657 if err != nil {
658 t.Error("Unexpected error:", err)
659 }
660 success, field, expectation, outcome := compareSessions(session, result)
661 if !success {
662 t.Errorf(`Expected field %s to be %v, but got %v`, field, expectation, outcome)
663 }
664 }
666 func TestBuildLoginRedirect(t *testing.T) {
667 t.Parallel()
668 req, err := http.NewRequest("GET", "https://client.secondbit.org/my/awesome/path?has=query¶ms=to&screw=this&all=up", nil)
669 if err != nil {
670 t.Error("Unexpected error creating base request:", err)
671 }
672 result := buildLoginRedirect(req, Context{})
673 if result != "" {
674 t.Error("Expected empty string as the result, got", result)
675 }
676 uri, err := url.Parse("https://auth.secondbit.org/login?query=string&other=param")
677 if err != nil {
678 t.Error("Unexpected error parsing URL:", err)
679 }
680 c := Context{loginURI: uri}
681 result = buildLoginRedirect(req, c)
682 expectation := "https://auth.secondbit.org/login?from=https%3A%2F%2Fclient.secondbit.org%2Fmy%2Fawesome%2Fpath%3Fhas%3Dquery%26params%3Dto%26screw%3Dthis%26all%3Dup&other=param&query=string"
683 if result != expectation {
684 t.Errorf(`Expected result string to be "%s", was "%s"`, expectation, result)
685 }
686 }
688 func TestAuthenticateHelper(t *testing.T) {
689 t.Parallel()
690 store := NewMemstore()
691 context := Context{
692 profiles: store,
693 }
694 profile := Profile{
695 ID: uuid.NewID(),
696 Name: "Test User",
697 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
698 Iterations: 1,
699 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
700 PassphraseScheme: 1,
701 Compromised: false,
702 LockedUntil: time.Time{},
703 PassphraseReset: "",
704 PassphraseResetCreated: time.Time{},
705 Created: time.Now(),
706 LastSeen: time.Time{},
707 }
708 login := Login{
709 Type: "email",
710 Value: "test@example.com",
711 ProfileID: profile.ID,
712 Created: time.Now(),
713 LastUsed: time.Time{},
714 }
715 err := context.SaveProfile(profile)
716 if err != nil {
717 t.Error("Error saving profile:", err)
718 }
719 err = context.AddLogin(login)
720 if err != nil {
721 t.Error("Error adding login:", err)
722 }
723 response, err := authenticate("test@example.com", "mysecurepassphrase", context)
724 if err != nil {
725 t.Error("Unexpected error:", err)
726 }
727 success, field, expectation, result := compareProfiles(profile, response)
728 if !success {
729 t.Errorf(`Expected field %s to be "%v", got "%v"`, field, expectation, result)
730 }
731 response, err = authenticate("test2@example.com", "mysecurepassphrase", context)
732 if err != ErrIncorrectAuth {
733 t.Error("Expected ErrIncorrectAuth, got", err)
734 }
735 response, err = authenticate("test@example.com", "not the right password", context)
736 if err != ErrIncorrectAuth {
737 t.Error("Expected ErrIncorrectAuth, got", err)
738 }
739 scheme := 1000
740 phrase := "doesn't really matter, the scheme doesn't exist"
741 change := ProfileChange{
742 PassphraseScheme: &scheme,
743 Passphrase: &phrase,
744 }
745 err = context.UpdateProfile(profile.ID, change)
746 if err != nil {
747 t.Error("Unexpected error:", err)
748 }
749 response, err = authenticate("test@example.com", "not the right password", context)
750 if err != ErrInvalidPassphraseScheme {
751 t.Error("Expected ErrIncorrectAuth, got", err)
752 }
753 }
755 func TestGetTokenHandler(t *testing.T) {
756 t.Parallel()
757 store := NewMemstore()
758 context := Context{
759 clients: store,
760 authCodes: store,
761 tokens: store,
762 }
763 client := Client{
764 ID: uuid.NewID(),
765 Secret: "sometimes I feel like I don't know what I'm doing",
766 OwnerID: uuid.NewID(),
767 Name: "A Super Awesome Client!",
768 Logo: "https://logos.secondbit.org/client.png",
769 Website: "https://client.secondbit.org/",
770 Type: "confidential",
771 }
772 authCode := AuthorizationCode{
773 Code: "testcode",
774 Created: time.Now(),
775 ExpiresIn: 600,
776 ClientID: client.ID,
777 Scope: "testscope",
778 RedirectURI: "https://client.secondbit.org/",
779 State: "teststate",
780 ProfileID: uuid.NewID(),
781 }
782 err := context.SaveAuthorizationCode(authCode)
783 if err != nil {
784 t.Error("Error saving auth code:", err)
785 }
786 err = context.SaveClient(client)
787 if err != nil {
788 t.Error("Error saving client:", err)
789 }
790 // BUG(paddy): We're only testing that GetTokenHandler returns the right values when we have the right input. But what about when we have the wrong input? We should test for invalid client errors and invalid grant errors to make sure they're triggered.
791 data := url.Values{}
792 data.Set("grant_type", "authorization_code")
793 data.Set("code", authCode.Code)
794 data.Set("redirect_uri", authCode.RedirectURI)
795 body := bytes.NewBufferString(data.Encode())
796 req, err := http.NewRequest("POST", "https://auth.secondbit.org/", ioutil.NopCloser(body))
797 if err != nil {
798 t.Error("Error constructing request:", err)
799 }
800 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
801 req.SetBasicAuth(client.ID.String(), client.Secret)
802 w := httptest.NewRecorder()
803 GetTokenHandler(w, req, context)
804 resp := tokenResponse{}
805 err = json.Unmarshal(w.Body.Bytes(), &resp)
806 if err != nil {
807 t.Error("Error unmarshalling response:", err)
808 t.Error("Response:", w.Body.String())
809 }
810 if resp.AccessToken == "" {
811 t.Error("Got blank access token back")
812 }
813 if resp.RefreshToken == "" {
814 t.Error("Got blank refresh token back")
815 }
816 if resp.TokenType == "" {
817 t.Error("Got blank token type back")
818 }
819 if resp.ExpiresIn == 0 {
820 t.Error("Got blank expires in back")
821 }
822 tokens, err := context.GetTokensByProfileID(authCode.ProfileID, 1, 0)
823 if err != nil {
824 t.Error("Error retrieving token:", err)
825 }
826 if len(tokens) != 1 {
827 t.Fatalf("Expected %d tokens, got %d", 1, len(tokens))
828 }
829 if tokens[0].AccessToken != resp.AccessToken {
830 t.Errorf(`Expected access token to be "%s", got "%s"`, tokens[0].AccessToken, resp.AccessToken)
831 }
832 if tokens[0].RefreshToken != resp.RefreshToken {
833 t.Errorf(`Expected refresh token to be "%s", got "%s"`, tokens[0].RefreshToken, resp.RefreshToken)
834 }
835 if tokens[0].ExpiresIn != resp.ExpiresIn {
836 t.Errorf(`Expected expires in to be %d, got %d`, tokens[0].ExpiresIn, resp.ExpiresIn)
837 }
838 if tokens[0].TokenType != resp.TokenType {
839 t.Errorf(`Expected token type to be %s, got %s`, tokens[0].TokenType, resp.TokenType)
840 }
841 // BUG(paddy): We need to test for the refresh_token grant type, too.
842 // BUG(paddy): We need to test for the password grant type, too.
843 // BUG(paddy): We need to test for the client_credentials grant type, too.
844 }