diff --git a/authn/authenticate.go b/authn/authenticate.go index 50373199c..5cc1d64aa 100644 --- a/authn/authenticate.go +++ b/authn/authenticate.go @@ -52,7 +52,8 @@ type Group struct { // Groups holds a slice of Group for marshalling from JSON type Groups struct { - Groups []Group `json:"value"` + NextLink string `json:"@odata.nextLink"` + Groups []Group `json:"value"` } // Auth holds state, configuration, and utilities needed for authentication. @@ -128,6 +129,9 @@ func (a *Auth) Authenticate(ctx context.Context, bearer string) (*UserInfo, erro if err != nil { return nil, errors.Wrap(err, "refreshing token") } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("refreshing token: %s", resp.Status) + } defer resp.Body.Close() var t oauth2.Token if err := json.NewDecoder(resp.Body).Decode(&t); err != nil { @@ -240,25 +244,39 @@ func (a *Auth) Redirect(w http.ResponseWriter, r *http.Request) { func (a *Auth) getGroups(token string) ([]Group, error) { var groups Groups - g, ok := a.groupsCache[token] - if ok && (time.Now().Sub(g.cacheTime) < a.cacheTTL) { - return g.groups, nil + gc, ok := a.groupsCache[token] + if ok && (time.Now().Sub(gc.cacheTime) < a.cacheTTL) && len(gc.groups) > 0 { + return gc.groups, nil } - req, err := http.NewRequest("GET", a.groupEndpoint, nil) - if err != nil { - return groups.Groups, errors.Wrap(err, "creating new request to group endpoint") + nextLink := a.groupEndpoint + for nextLink != "" { + req, err := http.NewRequest("GET", nextLink, nil) + if err != nil { + return nil, errors.Wrap(err, "creating new request to group endpoint") + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + response, err := http.DefaultClient.Do(req) + if err != nil { + return nil, errors.Wrap(err, "getting group membership info") + } + if response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("getting group membership info: %s", response.Status) + } + + var g Groups + if err = json.NewDecoder(response.Body).Decode(&g); err != nil { + return groups.Groups, errors.Wrap(err, "failed unmarshalling group membership response") + } + + response.Body.Close() + groups.Groups = append(groups.Groups, g.Groups...) + nextLink = g.NextLink } - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) - response, err := http.DefaultClient.Do(req) - if err != nil { - return groups.Groups, errors.Wrap(err, "getting group membership info") - } - - defer response.Body.Close() - if err = json.NewDecoder(response.Body).Decode(&groups); err != nil { - return groups.Groups, errors.Wrap(err, "failed unmarshalling group membership response") + if len(groups.Groups) == 0 { + return nil, fmt.Errorf("no groups found") } a.groupsCache[token] = cachedGroups{ diff --git a/authn/authenticate_internal_test.go b/authn/authenticate_internal_test.go index 44c192d28..57d7dd846 100644 --- a/authn/authenticate_internal_test.go +++ b/authn/authenticate_internal_test.go @@ -198,7 +198,7 @@ func TestAuthenticate(t *testing.T) { refresh: true, errOnRefresh: true, exp: -17764800, - err: fmt.Errorf("decoding refreshed token: invalid character 'b' looking for beginning of value"), + err: fmt.Errorf("refreshing token: 500 Internal Server Error"), }, } for _, test := range cases { @@ -352,19 +352,19 @@ func TestGetGroups(t *testing.T) { cacheTime: time.Now(), groups: []Group{ { - GroupID: "i feel it in the water", - GroupName: "i feel it in the earth", + GroupID: "a han noston ned wilith", + GroupName: "I smell it in the air", }, }, }, } - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srvNext := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := json.Marshal( Groups{ Groups: []Group{ { - GroupID: "much that once was is lost", - GroupName: "for none now live who remember it", + GroupID: "han mathon ne chae", + GroupName: "I feel it in the earth", }, }, }, @@ -374,6 +374,25 @@ func TestGetGroups(t *testing.T) { } fmt.Fprintf(w, "%s", body) })) + defer srvNext.Close() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := json.Marshal( + Groups{ + NextLink: srvNext.URL, + Groups: []Group{ + { + GroupID: "han mathon ne nen", + GroupName: "i feel it in the water", + }, + }, + }, + ) + if err != nil { + t.Fatalf("unexpected error marshalling groups response: %v", err) + } + fmt.Fprintf(w, "%s", body) + })) + defer srv.Close() a.groupEndpoint = srv.URL for name, test := range map[string]struct { @@ -384,8 +403,8 @@ func TestGetGroups(t *testing.T) { token: "the world is changed", groups: []Group{ { - GroupID: "i feel it in the water", - GroupName: "i feel it in the earth", + GroupID: "a han noston ned wilith", + GroupName: "I smell it in the air", }, }, }, @@ -393,8 +412,12 @@ func TestGetGroups(t *testing.T) { token: "i smell it in the air", groups: []Group{ { - GroupID: "much that once was is lost", - GroupName: "for none now live who remember it", + GroupID: "han mathon ne nen", + GroupName: "i feel it in the water", + }, + { + GroupID: "han mathon ne chae", + GroupName: "I feel it in the earth", }, }, }, diff --git a/http_handler_internal_test.go b/http_handler_internal_test.go index 882eebfe6..f496cf492 100644 --- a/http_handler_internal_test.go +++ b/http_handler_internal_test.go @@ -5,6 +5,7 @@ import ( "bytes" "encoding/hex" "encoding/json" + "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -190,12 +191,42 @@ func readResponse(w *httptest.ResponseRecorder) ([]byte, error) { func TestAuthentication(t *testing.T) { type evaluate func(w *httptest.ResponseRecorder, data []byte) type endpoint func(w http.ResponseWriter, r *http.Request) + + type Group struct { + GroupID string `json:"id"` + GroupName string `json:"displayName"` + } + + // Groups holds a slice of Group for marshalling from JSON + type Groups struct { + NextLink string `json:"@odata.nextLink"` + Groups []Group `json:"value"` + } + + groupSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := json.Marshal( + Groups{ + Groups: []Group{ + { + GroupID: "what are you?", + GroupName: "i am a carbon-based bipedal life form descended from an ape", + }, + }, + }, + ) + if err != nil { + t.Fatalf("unexpected error marshalling groups response: %v", err) + } + fmt.Fprintf(w, "%s", body) + })) + defer groupSrv.Close() + var ( ClientId = "e9088663-eb08-41d7-8f65-efb5f54bbb71" ClientSecret = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" AuthorizeURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize" TokenURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/token" - GroupEndpointURL = "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true" + GroupEndpointURL = groupSrv.URL LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout" Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"} SecretKey = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" @@ -650,29 +681,29 @@ func TestChkAuthN(t *testing.T) { } cases := []struct { - name string - endpoint string - token string - handler http.HandlerFunc - statusCode int + name string + endpoint string + token string + handler http.HandlerFunc + err string }{ { - name: "Valid", - token: validToken, - handler: h.chkAuthN(testingHandler), - statusCode: http.StatusOK, + name: "ValidToken-ButNotForMicrosoft", + token: validToken, + handler: h.chkAuthN(testingHandler), + err: "authenticating: getting groups: getting group membership info", }, { - name: "Invalid", - token: invalidToken, - handler: h.chkAuthN(testingHandler), - statusCode: http.StatusUnauthorized, + name: "Invalid", + token: invalidToken, + handler: h.chkAuthN(testingHandler), + err: "authenticating: parsing bearer token", }, { - name: "Expired", - token: expiredToken, - handler: h.chkAuthN(testingHandler), - statusCode: http.StatusUnauthorized, + name: "Expired", + token: expiredToken, + handler: h.chkAuthN(testingHandler), + err: "authenticating: token is expired", }, } for _, test := range cases { @@ -682,8 +713,13 @@ func TestChkAuthN(t *testing.T) { r.Header.Add("Authorization", test.token) test.handler(w, r) resp := w.Result() - if resp.StatusCode != test.statusCode { - t.Fatalf("expected %v, got %v", test.statusCode, resp.StatusCode) + body, err := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + if err != nil { + t.Fatal(err) + } + if !strings.HasPrefix(string(body), test.err) { + t.Fatalf("expected error %s, got: %s", test.err, string(body)) } }) }