Break out scopes and events.
This repo has gotten unwieldy, and there are portions of it that need to be
imported by a large number of other packages.
For example, scopes will be used in almost every API we write. Rather than
importing the entirety of this codebase into every API we write, I've opted to
move the scope logic out into a scopes package, with a subpackage for the
defined types, which is all most projects actually want to import.
We also define some event type constants, and importing those shouldn't require
a project to import all our dependencies, either. So I made an events subpackage
that just holds those constants.
This package has become a little bit of a red-headed stepchild and is do for a
refactor, but I'm trying to put that off as long as I can.
The refactoring of our scopes stuff has left a bug wherein a token can be
granted for scopes that don't exist. I'm going to need to revisit that, and also
how to limit scopes to only be granted to the users that should be able to
request them. But that's a battle for another day.
7 "github.com/gorilla/mux"
18 "code.secondbit.org/uuid.hg"
22 clientChangeSecret = 1 << iota
30 if os.Getenv("PG_TEST_DB") != "" {
31 p, err := NewPostgres(os.Getenv("PG_TEST_DB"))
35 clientStores = append(clientStores, &p)
39 var clientStores = []clientStore{NewMemstore()}
41 func compareClients(client1, client2 Client) (success bool, field string, val1, val2 interface{}) {
42 if !client1.ID.Equal(client2.ID) {
43 return false, "ID", client1.ID, client2.ID
45 if client1.Secret != client2.Secret {
46 return false, "secret", client1.Secret, client2.Secret
48 if !client1.OwnerID.Equal(client2.OwnerID) {
49 return false, "owner ID", client1.OwnerID, client2.OwnerID
51 if client1.Name != client2.Name {
52 return false, "name", client1.Name, client2.Name
54 if client1.Logo != client2.Logo {
55 return false, "logo", client1.Logo, client2.Logo
57 if client1.Website != client2.Website {
58 return false, "website", client1.Website, client2.Website
60 if client1.Type != client2.Type {
61 return false, "type", client1.Type, client2.Type
63 return true, "", nil, nil
66 func compareEndpoints(endpoint1, endpoint2 Endpoint) (success bool, field string, val1, val2 interface{}) {
67 if !endpoint1.ID.Equal(endpoint2.ID) {
68 return false, "ID", endpoint1.ID, endpoint2.ID
70 if !endpoint1.ClientID.Equal(endpoint2.ClientID) {
71 return false, "OwnerID", endpoint1.ClientID, endpoint2.ClientID
73 if !endpoint1.Added.Equal(endpoint2.Added) {
74 return false, "Added", endpoint1.Added, endpoint2.Added
76 if endpoint1.URI != endpoint2.URI {
77 return false, "URI", endpoint1.URI, endpoint2.URI
79 return true, "", nil, nil
82 func TestClientStoreSuccess(t *testing.T) {
87 OwnerID: uuid.NewID(),
92 for _, store := range clientStores {
93 context := Context{clients: store}
94 err := context.SaveClient(client)
96 t.Fatalf("Error saving client to %T: %s", store, err)
98 err = context.SaveClient(client)
99 if err != ErrClientAlreadyExists {
100 t.Fatalf("Expected ErrClientAlreadyExists, got %v from %T", err, store)
102 retrieved, err := context.GetClient(client.ID)
104 t.Fatalf("Error retrieving client from %T: %s", store, err)
106 success, field, expectation, result := compareClients(client, retrieved)
108 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
110 clients, err := context.ListClientsByOwner(client.OwnerID, 25, 0)
112 t.Fatalf("Error retrieving clients by owner from %T: %s", store, err)
114 if len(clients) != 1 {
115 t.Fatalf("Expected 1 client in response from %T, got %+v", store, clients)
117 success, field, expectation, result = compareClients(client, clients[0])
119 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
122 err = context.UpdateClient(client.ID, ClientChange{Deleted: &deleted})
124 t.Fatalf("Error deleting client from %T: %s", store, err)
126 retrieved, err = context.GetClient(client.ID)
127 if err != ErrClientNotFound {
128 t.Fatalf("Expected ErrClientNotFound from %T, got %+v and %s", store, retrieved, err)
130 clients, err = context.ListClientsByOwner(client.OwnerID, 25, 0)
132 t.Fatalf("Error listing clients by owner from %T: %s", store, err)
134 if len(clients) != 0 {
135 t.Fatalf("Expected 0 clients in response from %T, got %+v", store, clients)
140 func TestEndpointStoreSuccess(t *testing.T) {
145 OwnerID: uuid.NewID(),
150 endpoint1 := Endpoint{
153 Added: time.Now().Round(time.Millisecond),
154 URI: "https://www.example.com/",
156 endpoint2 := Endpoint{
159 Added: time.Now().Round(time.Millisecond),
160 URI: "https://www.example.com/my/full/path",
162 for _, store := range clientStores {
163 context := Context{clients: store}
164 err := context.SaveClient(client)
166 t.Fatalf("Error saving client to %T: %s", store, err)
168 err = context.AddEndpoints([]Endpoint{endpoint1})
170 t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
172 endpoints, err := context.ListEndpoints(client.ID, 10, 0)
174 t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
176 if len(endpoints) != 1 {
177 t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
179 success, field, expectation, result := compareEndpoints(endpoint1, endpoints[0])
181 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
183 err = context.AddEndpoints([]Endpoint{endpoint2})
185 t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
187 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
189 t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
191 if len(endpoints) != 2 {
192 t.Fatalf("Expected %d endpoints, got %+v from %T", 2, endpoints, store)
194 sortedEnd := sortedEndpoints(endpoints)
196 endpoints = []Endpoint(sortedEnd)
197 success, field, expectation, result = compareEndpoints(endpoint1, endpoints[0])
199 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
201 success, field, expectation, result = compareEndpoints(endpoint2, endpoints[1])
203 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
205 err = context.RemoveEndpoint(client.ID, endpoint1.ID)
207 t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
209 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
211 t.Fatalf("Error listing endpoints in %T: %s", store, err)
213 if len(endpoints) != 1 {
214 t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
216 success, field, expectation, result = compareEndpoints(endpoint2, endpoints[0])
218 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
220 err = context.RemoveEndpoint(client.ID, endpoint2.ID)
222 t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
224 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
226 t.Fatalf("Error listing endpoints in %T: %s", store, err)
228 if len(endpoints) != 0 {
229 t.Fatalf("Expected %d endpoints, got %+v from %T", 0, endpoints, store)
234 func TestClientUpdates(t *testing.T) {
240 OwnerID: uuid.NewID(),
245 for i := 0; i < variations; i++ {
246 var secret, name, logo, website string
247 change := ClientChange{}
248 client.ID = uuid.NewID()
249 expectation := client
251 if i&clientChangeSecret != 0 {
252 secret = fmt.Sprintf("secret-%d", i)
253 change.Secret = &secret
254 expectation.Secret = secret
256 if i&clientChangeOwnerID != 0 {
257 change.OwnerID = uuid.NewID()
258 expectation.OwnerID = change.OwnerID
260 if i&clientChangeName != 0 {
261 name = fmt.Sprintf("name-%d", i)
263 expectation.Name = name
265 if i&clientChangeLogo != 0 {
266 logo = fmt.Sprintf("logo-%d", i)
268 expectation.Logo = logo
270 if i&clientChangeWebsite != 0 {
271 website = fmt.Sprintf("website-%d", i)
272 change.Website = &website
273 expectation.Website = website
275 result.ApplyChange(change)
276 match, field, expected, got := compareClients(expectation, result)
278 t.Fatalf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
280 for _, store := range clientStores {
281 context := Context{clients: store}
282 err := context.SaveClient(client)
284 t.Fatalf("Error saving client in %T: %s", store, err)
286 err = context.UpdateClient(client.ID, change)
288 t.Fatalf("Error updating client in %T: %s", store, err)
290 retrieved, err := context.GetClient(client.ID)
292 t.Fatalf("Error getting client from %T: %s", store, err)
294 match, field, expected, got = compareClients(expectation, retrieved)
296 t.Fatalf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
299 err = context.UpdateClient(client.ID, ClientChange{Deleted: &deleted})
301 t.Fatalf("Error deleting client from %T: %s", store, err)
307 func TestClientEndpointChecks(t *testing.T) {
312 OwnerID: uuid.NewID(),
317 endpoint1 := Endpoint{
320 Added: time.Now().Round(time.Millisecond),
321 URI: "https://www.example.com/first",
323 endpoint2 := Endpoint{
326 Added: time.Now().Round(time.Millisecond),
327 URI: "https://www.example.com/my/full/path",
329 candidates := map[string]bool{
330 "https://www.example.com/": false,
331 "https://www.example.com/first": true,
332 "https://www.example.com/first/extra/path": false,
333 "https://www.example.com/my": false,
334 "https://www.example.com/my/full/path": true,
336 for _, store := range clientStores {
337 context := Context{clients: store}
338 err := context.SaveClient(client)
340 t.Fatalf("Error saving client in %T: %s", store, err)
342 err = context.AddEndpoints([]Endpoint{endpoint1})
344 t.Fatalf("Error saving endpoint in %T: %s", store, err)
346 err = context.AddEndpoints([]Endpoint{endpoint2})
348 t.Fatalf("Error saving endpoint in %T: %s", store, err)
350 for candidate, expectation := range candidates {
351 result, err := context.CheckEndpoint(client.ID, candidate)
353 t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
355 if result != expectation {
362 t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
368 func TestClientEndpointChecksStrict(t *testing.T) {
373 OwnerID: uuid.NewID(),
378 endpoint1 := Endpoint{
381 Added: time.Now().Round(time.Millisecond),
382 URI: "https://www.example.com/first",
384 endpoint2 := Endpoint{
387 Added: time.Now().Round(time.Millisecond),
388 URI: "https://www.example.com/my/full/path",
390 candidates := map[string]bool{
391 "https://www.example.com/": false,
392 "https://www.example.com/first": true,
393 "https://www.example.com/first/extra/path": false,
394 "https://www.example.com/my": false,
395 "https://www.example.com/my/full/path": true,
397 for _, store := range clientStores {
398 context := Context{clients: store}
399 err := context.SaveClient(client)
401 t.Fatalf("Error saving client in %T: %s", store, err)
403 err = context.AddEndpoints([]Endpoint{endpoint1})
405 t.Fatalf("Error saving endpoint in %T: %s", store, err)
407 err = context.AddEndpoints([]Endpoint{endpoint2})
409 t.Fatalf("Error saving endpoint in %T: %s", store, err)
411 for candidate, expectation := range candidates {
412 result, err := context.CheckEndpoint(client.ID, candidate)
414 t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
416 if result != expectation {
423 t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
429 func TestClientChangeValidation(t *testing.T) {
431 change := ClientChange{}
432 if err := change.Validate(); err[0] != ErrEmptyChange {
433 t.Errorf("Expected %s to give an error of %s, gave %s", "empty change", ErrEmptyChange, err)
435 names := map[string][]error{
436 "a": []error{ErrClientNameTooShort},
439 "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopq": []error{ErrClientNameTooLong},
441 for name, expectation := range names {
442 change = ClientChange{Name: &name}
443 errs := change.Validate()
444 if len(errs) != len(expectation) {
445 t.Errorf("Expected %s to give %d errors, gave %d", name, len(expectation), len(errs))
448 for pos, err := range errs {
449 if err != expectation[pos] {
450 t.Errorf("Expected %s to give an error of %s in position %d, gave %s", name, expectation[pos], pos, err)
455 for i := 0; i < 1025; i++ {
456 longPath = fmt.Sprintf("%s%d", longPath, i)
458 logos := map[string][]error{
459 "https://www.example.com/" + longPath: []error{ErrClientLogoTooLong},
460 "https://www.example.com/ab": []error{},
461 "www.example.com/ab": []error{ErrClientLogoNotURL},
462 "test": []error{ErrClientLogoNotURL},
465 for logo, expectation := range logos {
466 change = ClientChange{Logo: &logo}
467 errs := change.Validate()
468 if len(errs) != len(expectation) {
469 t.Errorf("Expected %s to give %d errors, gave %d", logo, len(expectation), len(errs))
471 for pos, err := range errs {
472 if err != expectation[pos] {
473 t.Errorf("Expected %s to give an error of %s in positiong %d, gave %s", logo, expectation[pos], pos, err)
477 websites := map[string][]error{
478 "https://www.example.com/" + longPath: []error{ErrClientWebsiteTooLong},
479 "https://www.example.com/ab": []error{},
480 "www.example.com/ab": []error{ErrClientWebsiteNotURL},
481 "test": []error{ErrClientWebsiteNotURL},
484 for website, expectation := range websites {
485 change = ClientChange{Website: &website}
486 errs := change.Validate()
487 if len(errs) != len(expectation) {
488 t.Errorf("Expected %s to give %d errors, gave %d", website, len(expectation), len(errs))
490 for pos, err := range errs {
491 if err != expectation[pos] {
492 t.Errorf("Expected %s to give an error of %s in position %d, gave %s", website, expectation[pos], pos, err)
498 func TestGetClientAuth(t *testing.T) {
500 type clientAuthRequest struct {
505 expectedClientID uuid.ID
506 expectedClientSecret string
510 expectAuthenticateHeader bool
513 tests := []clientAuthRequest{
514 {"", "", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
515 {"", "", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
516 {"", "no clientID set", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
517 {"", "no clientID set", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
518 {"not an actual id", "invalid client ID set", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
519 {"not an actual id", "invalid client ID set", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
520 {"", "", "not an actual id", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
521 {id.String(), "secret", "", true, id, "secret", true, http.StatusOK, "", false},
522 {id.String(), "secret", "", false, id, "secret", true, http.StatusOK, "", false},
523 {"", "", id.String(), true, id, "", true, http.StatusOK, "", false},
524 {"", "", id.String(), false, nil, "", false, http.StatusBadRequest, `{"error":"unauthorized_client"}`, false},
526 for pos, test := range tests {
527 t.Logf("Running test #%d, with request %+v", pos, test)
528 w := httptest.NewRecorder()
529 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
531 t.Fatal("Can't build request:", err)
533 if test.username != "" || test.pass != "" {
534 r.SetBasicAuth(test.username, test.pass)
536 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
537 params := url.Values{}
538 params.Set("client_id", test.clientID)
539 body := bytes.NewBufferString(params.Encode())
540 r.Body = ioutil.NopCloser(body)
541 respID, respSecret, success := getClientAuth(w, r, test.allowPublic)
542 if (respID == nil && test.expectedClientID != nil) || (respID != nil && test.expectedClientID == nil) || !respID.Equal(test.expectedClientID) {
543 t.Errorf("Expected response ID to be %v, got %v", test.expectedClientID, respID)
545 if test.expectedClientSecret != respSecret {
546 t.Errorf("Expected response secret to be '%s', got '%s'", test.expectedClientSecret, respSecret)
548 if test.expectedValid != success {
549 t.Errorf("Expected success result to be %v, got %v", test.expectedValid, success)
551 if test.expectedCode != w.Code {
552 t.Errorf("Expected response code to be %d, got %d", test.expectedCode, w.Code)
554 if test.expectedBody != strings.TrimSpace(w.Body.String()) {
555 t.Errorf("Expected body to be '%s', got '%s'", test.expectedBody, strings.TrimSpace(w.Body.String()))
557 if test.expectAuthenticateHeader && w.Header().Get("WWW-Authenticate") != "Basic" {
558 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
563 func TestVerifyClient(t *testing.T) {
565 type verifyClientRequest struct {
570 expectedClientID uuid.ID
574 expectAuthenticateHeader bool
576 memstore := NewMemstore()
582 Secret: "super secret!",
583 OwnerID: uuid.NewID(),
584 Name: "My test client",
585 Logo: "https://secondbit.org/logo.png",
586 Website: "https://secondbit.org/",
587 Type: "confidential",
589 err := context.SaveClient(client)
591 t.Fatal("Could not save client:", err)
593 publicClient := Client{
596 OwnerID: uuid.NewID(),
597 Name: "A public client",
598 Logo: "https://secondbit.org/logo.png",
599 Website: "https://secondbit.org/",
602 err = context.SaveClient(publicClient)
604 t.Fatal("Could not save client:", err)
607 tests := []verifyClientRequest{
608 {"", "", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
609 {"", "", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
610 {"", "no clientID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
611 {"", "no clientID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
612 {"not an actual id", "invalid client ID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
613 {"not an actual id", "invalid client ID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
614 {id.String(), "unsaved client ID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
615 {id.String(), "unsaved client ID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
616 {client.ID.String(), "wrong secret", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
617 {client.ID.String(), "wrong secret", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
618 {"", "", "not an actual id", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
619 {"", "", id.String(), true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
620 {client.ID.String(), client.Secret, "", true, client.ID, true, http.StatusOK, "", false},
621 {client.ID.String(), client.Secret, "", false, client.ID, true, http.StatusOK, "", false},
622 {"", "", publicClient.ID.String(), true, publicClient.ID, true, http.StatusOK, "", false},
623 {"", "", publicClient.ID.String(), false, nil, false, http.StatusBadRequest, `{"error":"unauthorized_client"}`, false},
626 for pos, test := range tests {
627 t.Logf("Running test #%d, with request %+v", pos, test)
628 w := httptest.NewRecorder()
629 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
631 t.Fatal("Can't build request:", err)
633 if test.username != "" || test.pass != "" {
634 r.SetBasicAuth(test.username, test.pass)
636 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
637 params := url.Values{}
638 params.Set("client_id", test.clientID)
639 body := bytes.NewBufferString(params.Encode())
640 r.Body = ioutil.NopCloser(body)
641 respID, success := verifyClient(w, r, test.allowPublic, context)
642 if (respID == nil && test.expectedClientID != nil) || (respID != nil && test.expectedClientID == nil) || !respID.Equal(test.expectedClientID) {
643 t.Errorf("Expected response ID to be %v, got %v", test.expectedClientID, respID)
645 if test.expectedValid != success {
646 t.Errorf("Expected success result to be %v, got %v", test.expectedValid, success)
648 if test.expectedCode != w.Code {
649 t.Errorf("Expected response code to be %d, got %d", test.expectedCode, w.Code)
651 if test.expectedBody != strings.TrimSpace(w.Body.String()) {
652 t.Errorf("Expected body to be '%s', got '%s'", test.expectedBody, strings.TrimSpace(w.Body.String()))
654 if test.expectAuthenticateHeader && w.Header().Get("WWW-Authenticate") != "Basic" {
655 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
660 func TestCreateClientHandler(t *testing.T) {
662 memstore := NewMemstore()
667 w := httptest.NewRecorder()
668 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/clients", nil)
670 t.Fatal("Can't build request:", err)
672 r.Header.Set("Content-Type", "application/json")
673 CreateClientHandler(w, r, c)
674 if w.Code != http.StatusUnauthorized {
675 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
677 expected := `{"errors":[{"error":"access_denied"}]}`
678 result := strings.TrimSpace(w.Body.String())
679 if result != expected {
680 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
682 w = httptest.NewRecorder()
683 r.Header.Set("Authorization", "Not basic at all...")
684 CreateClientHandler(w, r, c)
685 if w.Code != http.StatusUnauthorized {
686 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
688 expected = `{"errors":[{"error":"access_denied"}]}`
689 result = strings.TrimSpace(w.Body.String())
690 if result != expected {
691 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
693 w = httptest.NewRecorder()
694 r.Header.Set("Authorization", "Basic TotallyNotBase64Encoded")
695 CreateClientHandler(w, r, c)
696 if w.Code != http.StatusUnauthorized {
697 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
699 expected = `{"errors":[{"error":"access_denied"}]}`
700 result = strings.TrimSpace(w.Body.String())
701 if result != expected {
702 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
704 w = httptest.NewRecorder()
705 r.Header.Set("Authorization", "Basic dGhpc2hhc25vY29sb24=")
706 CreateClientHandler(w, r, c)
707 if w.Code != http.StatusUnauthorized {
708 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
710 expected = `{"errors":[{"error":"access_denied"}]}`
711 result = strings.TrimSpace(w.Body.String())
712 if result != expected {
713 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
718 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
720 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
723 LockedUntil: time.Time{},
725 PassphraseResetCreated: time.Time{},
726 Created: time.Now().Round(time.Millisecond),
727 LastSeen: time.Time{},
731 Value: "test@example.com",
732 ProfileID: profile.ID,
733 Created: time.Now().Round(time.Millisecond),
734 LastUsed: time.Time{},
736 w = httptest.NewRecorder()
737 r.SetBasicAuth("test@example.com", "mysecurepassphrase")
738 CreateClientHandler(w, r, c)
739 if w.Code != http.StatusUnauthorized {
740 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
742 expected = `{"errors":[{"error":"access_denied"}]}`
743 result = strings.TrimSpace(w.Body.String())
744 if result != expected {
745 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
747 err = c.SaveProfile(profile)
749 t.Error("Error saving profile:", err)
751 err = c.AddLogin(login)
753 t.Error("Error adding login:", err)
755 r.SetBasicAuth("test@example.com", "mysecurepassphrase")
756 type testStruct struct {
761 tests := []testStruct{
762 {``, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidFormat, Field: "/"}}}},
763 {`{}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrMissing, Field: "/type"}, {Slug: RequestErrMissing, Field: "/name"}}}},
764 {`{"type":"notarealtype"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidValue, Field: "/type"}, {Slug: RequestErrMissing, Field: "/name"}}}},
765 {`{"type":"notarealtype","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidValue, Field: "/type"}, {Slug: RequestErrOverflow, Field: "/name"}}}},
766 {`{"type":"notarealtype","name":"a"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidValue, Field: "/type"}, {Slug: RequestErrInsufficient, Field: "/name"}}}},
767 {`{"type":"public"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrMissing, Field: "/name"}}}},
768 {`{"type":"public","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrOverflow, Field: "/name"}}}},
769 {`{"type":"public","name":"a"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInsufficient, Field: "/name"}}}},
770 {`{"name":"My Client"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrMissing, Field: "/type"}}}},
771 {`{"type":"notarealtype","name":"My Client"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidValue, Field: "/type"}}}},
772 {`{"type":"public","name":"My Client"}`, http.StatusCreated, Response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}}},
773 {`{"type":"public","name":"My Client", "endpoints": ["https://test.secondbit.org/", "https://paddy.io"]}`, http.StatusCreated, Response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://test.secondbit.org/"}, {URI: "https://paddy.io"}}}},
774 {`{"type":"public","name":"My Client", "endpoints": [":/not a url", "https://paddy.io"]}`, http.StatusCreated, Response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://paddy.io"}}, Errors: []RequestError{{Slug: RequestErrInvalidFormat, Field: "/endpoints/0"}}}},
775 {`{"type":"public","name":"My Client", "endpoints": [":/not a url", "/relative/uri", "https://paddy.io"]}`, http.StatusCreated, Response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://paddy.io"}}, Errors: []RequestError{{Slug: RequestErrInvalidFormat, Field: "/endpoints/0"}, {Slug: RequestErrInvalidValue, Field: "/endpoints/1"}}}},
776 {`{"type":"confidential","name":"Secret Client", "endpoints": ["https://secondbit.org"]}`, http.StatusCreated, Response{Clients: []Client{{Name: "Secret Client", OwnerID: profile.ID, Type: "confidential"}}, Endpoints: []Endpoint{{URI: "https://secondbit.org"}}}},
778 for pos, test := range tests {
779 t.Logf("Test #%d: `%s`", pos, test.request)
780 w = httptest.NewRecorder()
781 body := bytes.NewBufferString(test.request)
782 r.Body = ioutil.NopCloser(body)
783 CreateClientHandler(w, r, c)
784 if w.Code != test.code {
785 t.Errorf("Expected response code to be %d, got %d", test.code, w.Code)
787 t.Logf("Response: %s", w.Body.String())
789 err = json.Unmarshal(w.Body.Bytes(), &res)
791 t.Error("Unexpected error unmarshalling response:", err)
793 if len(res.Clients) > 0 {
794 if res.Clients[0].Type == "confidential" && res.Clients[0].Secret == "" {
795 t.Log("Client:", res.Clients[0])
796 t.Error("Expected confidential client to have a secret, but does not.")
797 } else if res.Clients[0].Type == "public" && res.Clients[0].Secret != "" {
798 t.Log("Client:", res.Clients[0])
799 t.Error("Expected public client to not have a secret, but it does.")
802 fillInServerGenerated(test.resp, res)
803 success, field, expectation, result := compareResponses(test.resp, res)
805 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
810 func TestGetClientHandler(t *testing.T) {
812 memstore := NewMemstore()
819 Secret: "myawesomesecret",
820 OwnerID: uuid.NewID(),
822 Logo: "https://auth.secondbit.org/logo.png",
823 Website: "https://code.secondbit.org",
824 Type: clientTypeConfidential,
826 err := c.SaveClient(client)
828 t.Fatal("Can't store client in memstore:", err)
833 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
835 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
838 LockedUntil: time.Time{},
840 PassphraseResetCreated: time.Time{},
841 Created: time.Now().Round(time.Millisecond),
842 LastSeen: time.Time{},
846 Value: "test@example.com",
847 ProfileID: profile.ID,
848 Created: time.Now().Round(time.Millisecond),
849 LastUsed: time.Time{},
851 err = c.SaveProfile(profile)
853 t.Error("Error saving profile:", err)
855 err = c.AddLogin(login)
857 t.Error("Error adding login:", err)
859 router := mux.NewRouter()
860 RegisterClientHandlers(router, c)
861 w := httptest.NewRecorder()
862 u := "https://test.auth.secondbit.org/clients/" + client.ID.String()
863 r, err := http.NewRequest("GET", u, nil)
865 t.Fatal("Can't build request:", err)
867 r.Header.Set("Content-Type", "application/json")
868 router.ServeHTTP(w, r)
869 if w.Code != http.StatusOK {
870 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
872 t.Logf("Response: %s", w.Body.String())
874 err = json.Unmarshal(w.Body.Bytes(), &res)
876 t.Error("Unexpected error unmarshalling response:", err)
878 if len(res.Clients) != 1 {
879 t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
881 if res.Clients[0].Secret != "" {
882 t.Error("Expected secret not to be set, but was set to", res.Clients[0].Secret)
884 // fill in the secret, which was omitted in the response
885 res.Clients[0].Secret = client.Secret
886 success, field, expectation, result := compareClients(client, res.Clients[0])
888 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
891 // test for improperly formatted ID
892 u = "https://test.auth.secondbit.org/clients/notanID"
893 w = httptest.NewRecorder()
894 r, err = http.NewRequest("GET", u, nil)
896 t.Fatal("Can't build request:", err)
898 r.Header.Set("Content-Type", "application/json")
899 router.ServeHTTP(w, r)
900 if w.Code != http.StatusBadRequest {
901 t.Errorf("Expected response code to be %d, got %d", http.StatusBadRequest, w.Code)
903 t.Logf("Response: %s", w.Body.String())
905 err = json.Unmarshal(w.Body.Bytes(), &res)
907 t.Error("Unexpected error unmarshalling response:", err)
909 if len(res.Errors) != 1 {
910 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
912 e := RequestError{Slug: RequestErrInvalidFormat, Param: "id"}
913 success, field, expectation, result = compareErrors(e, res.Errors[0])
915 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
918 // test for a non-existent client
919 u = "https://test.auth.secondbit.org/clients/" + uuid.NewID().String()
920 w = httptest.NewRecorder()
921 r, err = http.NewRequest("GET", u, nil)
923 t.Fatal("Can't build request:", err)
925 r.Header.Set("Content-Type", "application/json")
926 router.ServeHTTP(w, r)
927 if w.Code != http.StatusNotFound {
928 t.Errorf("Expected response code to be %d, got %d", http.StatusNotFound, w.Code)
930 t.Logf("Response: %s", w.Body.String())
932 err = json.Unmarshal(w.Body.Bytes(), &res)
934 t.Error("Unexpected error unmarshalling response:", err)
936 if len(res.Errors) != 1 {
937 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
939 e = RequestError{Slug: RequestErrNotFound, Param: "id"}
940 success, field, expectation, result = compareErrors(e, res.Errors[0])
942 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
946 func TestAuthenticatedGetClientHandler(t *testing.T) {
948 memstore := NewMemstore()
955 Secret: "myawesomesecret",
956 OwnerID: uuid.NewID(),
958 Logo: "https://auth.secondbit.org/logo.png",
959 Website: "https://code.secondbit.org",
960 Type: clientTypeConfidential,
962 err := c.SaveClient(client)
964 t.Fatal("Can't store client in memstore:", err)
969 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
971 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
974 LockedUntil: time.Time{},
976 PassphraseResetCreated: time.Time{},
977 Created: time.Now().Round(time.Millisecond),
978 LastSeen: time.Time{},
982 Value: "test@example.com",
983 ProfileID: profile.ID,
984 Created: time.Now().Round(time.Millisecond),
985 LastUsed: time.Time{},
987 err = c.SaveProfile(profile)
989 t.Error("Error saving profile:", err)
991 err = c.AddLogin(login)
993 t.Error("Error adding login:", err)
998 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
1000 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
1001 PassphraseScheme: 1,
1003 LockedUntil: time.Time{},
1004 PassphraseReset: "",
1005 PassphraseResetCreated: time.Time{},
1006 Created: time.Now().Round(time.Millisecond),
1007 LastSeen: time.Time{},
1011 Value: "test2@example.com",
1012 ProfileID: profile2.ID,
1013 Created: time.Now().Round(time.Millisecond),
1014 LastUsed: time.Time{},
1016 err = c.SaveProfile(profile2)
1018 t.Error("Error saving profile:", err)
1020 err = c.AddLogin(login2)
1022 t.Error("Error adding login:", err)
1024 router := mux.NewRouter()
1025 RegisterClientHandlers(router, c)
1026 w := httptest.NewRecorder()
1027 u := "https://test.auth.secondbit.org/clients/" + client.ID.String()
1028 r, err := http.NewRequest("GET", u, nil)
1030 t.Fatal("Can't build request:", err)
1032 r.Header.Set("Content-Type", "application/json")
1033 r.SetBasicAuth(login.Value, "mysecurepassphrase")
1034 router.ServeHTTP(w, r)
1035 if w.Code != http.StatusOK {
1036 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
1038 t.Logf("Response: %s", w.Body.String())
1040 err = json.Unmarshal(w.Body.Bytes(), &res)
1042 t.Error("Unexpected error unmarshalling response:", err)
1044 if len(res.Clients) != 1 {
1045 t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
1047 success, field, expectation, result := compareClients(client, res.Clients[0])
1049 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
1052 // test for improperly formatted ID
1053 u = "https://test.auth.secondbit.org/clients/notanID"
1054 w = httptest.NewRecorder()
1055 r, err = http.NewRequest("GET", u, nil)
1057 t.Fatal("Can't build request:", err)
1059 r.Header.Set("Content-Type", "application/json")
1060 r.SetBasicAuth(login.Value, "mysecurepassphrase")
1061 router.ServeHTTP(w, r)
1062 if w.Code != http.StatusBadRequest {
1063 t.Errorf("Expected response code to be %d, got %d", http.StatusBadRequest, w.Code)
1065 t.Logf("Response: %s", w.Body.String())
1067 err = json.Unmarshal(w.Body.Bytes(), &res)
1069 t.Error("Unexpected error unmarshalling response:", err)
1071 if len(res.Errors) != 1 {
1072 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
1074 e := RequestError{Slug: RequestErrInvalidFormat, Param: "id"}
1075 success, field, expectation, result = compareErrors(e, res.Errors[0])
1077 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
1080 // test for a non-existent client
1081 u = "https://test.auth.secondbit.org/clients/" + uuid.NewID().String()
1082 w = httptest.NewRecorder()
1083 r, err = http.NewRequest("GET", u, nil)
1085 t.Fatal("Can't build request:", err)
1087 r.Header.Set("Content-Type", "application/json")
1088 r.SetBasicAuth(login.Value, "mysecurepassphrase")
1089 router.ServeHTTP(w, r)
1090 if w.Code != http.StatusNotFound {
1091 t.Errorf("Expected response code to be %d, got %d", http.StatusNotFound, w.Code)
1093 t.Logf("Response: %s", w.Body.String())
1095 err = json.Unmarshal(w.Body.Bytes(), &res)
1097 t.Error("Unexpected error unmarshalling response:", err)
1099 if len(res.Errors) != 1 {
1100 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
1102 e = RequestError{Slug: RequestErrNotFound, Param: "id"}
1103 success, field, expectation, result = compareErrors(e, res.Errors[0])
1105 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
1108 // test for a wrong password
1109 u = "https://test.auth.secondbit.org/clients/" + client.ID.String()
1110 w = httptest.NewRecorder()
1111 r, err = http.NewRequest("GET", u, nil)
1113 t.Fatal("Can't build request:", err)
1115 r.Header.Set("Content-Type", "application/json")
1116 r.SetBasicAuth(login.Value, "notmypassphrase")
1117 router.ServeHTTP(w, r)
1118 if w.Code != http.StatusUnauthorized {
1119 t.Errorf("Expected response code to be %d, got %d", http.StatusUnauthorized, w.Code)
1121 t.Logf("Response: %s", w.Body.String())
1123 err = json.Unmarshal(w.Body.Bytes(), &res)
1125 t.Error("Unexpected error unmarshalling response:", err)
1127 if len(res.Errors) != 1 {
1128 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
1130 e = RequestError{Slug: RequestErrAccessDenied}
1131 success, field, expectation, result = compareErrors(e, res.Errors[0])
1133 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
1136 // test for a wrong account
1137 u = "https://test.auth.secondbit.org/clients/" + client.ID.String()
1138 w = httptest.NewRecorder()
1139 r, err = http.NewRequest("GET", u, nil)
1141 t.Fatal("Can't build request:", err)
1143 r.Header.Set("Content-Type", "application/json")
1144 r.SetBasicAuth(login2.Value, "mysecurepassphrase")
1145 router.ServeHTTP(w, r)
1146 if w.Code != http.StatusOK {
1147 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
1149 t.Logf("Response: %s", w.Body.String())
1151 err = json.Unmarshal(w.Body.Bytes(), &res)
1153 t.Error("Unexpected error unmarshalling response:", err)
1155 if len(res.Clients) != 1 {
1156 t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
1158 if res.Clients[0].Secret != "" {
1159 t.Errorf("Expected client secret to be empty, got %s", res.Clients[0].Secret)
1161 // fill the client's secret for comparison
1162 res.Clients[0].Secret = client.Secret
1163 success, field, expectation, result = compareClients(client, res.Clients[0])
1165 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
1169 // BUG(paddy): We need to test the clientCredentialsValidate function.
1170 // BUG(paddy): We need to test the ListClientsHandler.