diff --git a/oauth2/heroku.go b/oauth2/heroku.go new file mode 100644 index 000000000..83400a149 --- /dev/null +++ b/oauth2/heroku.go @@ -0,0 +1,74 @@ +package oauth2 + +import ( + "encoding/json" + "net/http" + + "github.com/influxdata/chronograf" + + "golang.org/x/oauth2" +) + +// Ensure that Heroku is an oauth2.Provider +var _ Provider = &Heroku{} + +const ( + // Routes required for interacting with Heroku API + HEROKU_ACCOUNT_ROUTE string = "https://api.heroku.com/account" +) + +// Heroku is an OAuth2 Provider allowing users to authenticate with Heroku to +// gain access to Chronograf +type Heroku struct { + // OAuth2 Secrets + ClientID string + ClientSecret string + + Logger chronograf.Logger +} + +// Config returns the OAuth2 exchange information and endpoints +func (h *Heroku) Config() *oauth2.Config { + return &oauth2.Config{} +} + +// ID returns the Heroku application client ID +func (h *Heroku) ID() string { + return h.ClientID +} + +// Name returns the name of this provider (heroku) +func (h *Heroku) Name() string { + return "heroku" +} + +// PrincipalID returns the Heroku email address of the user. +func (h *Heroku) PrincipalID(provider *http.Client) (string, error) { + resp, err := provider.Get(HEROKU_ACCOUNT_ROUTE) + if err != nil { + h.Logger.Error("Unable to communicate with Heroku. err:", err) + return "", err + } + defer resp.Body.Close() + d := json.NewDecoder(resp.Body) + var account struct { + Email string `json:"email"` + } + if err := d.Decode(&account); err != nil { + h.Logger.Error("Unable to decode response from Heroku. err:", err) + return "", err + } + return account.Email, nil +} + +// Scopes for heroku is "identity" which grants access to user account +// information. This will grant us access to the user's email address which is +// used as the Principal's identifier. +func (h *Heroku) Scopes() []string { + return []string{"identity"} +} + +// Secret returns the Heroku application client secret +func (h *Heroku) Secret() string { + return h.ClientSecret +} diff --git a/oauth2/heroku_test.go b/oauth2/heroku_test.go new file mode 100644 index 000000000..5beff997c --- /dev/null +++ b/oauth2/heroku_test.go @@ -0,0 +1,74 @@ +package oauth2_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/influxdata/chronograf/oauth2" +) + +func NewTestTripper(ts *httptest.Server, rt http.RoundTripper) (*TestTripper, error) { + url, err := url.Parse(ts.URL) + if err != nil { + return nil, err + } + return &TestTripper{rt, url}, nil +} + +type TestTripper struct { + rt http.RoundTripper + tsURL *url.URL +} + +// RoundTrip modifies the Hostname of the incoming request to be directed to the +// test server. +func (tt *TestTripper) RoundTrip(r *http.Request) (*http.Response, error) { + r.URL.Host = tt.tsURL.Host + r.URL.Scheme = tt.tsURL.Scheme + + return tt.rt.RoundTrip(r) +} + +func Test_Heroku_PrincipalID_ExtractsEmailAddress(t *testing.T) { + t.Parallel() + + expected := struct { + Email string `json:"email"` + }{ + "martymcfly@example.com", + } + + mockAPI := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/account" { + rw.WriteHeader(http.StatusNotFound) + return + } + enc := json.NewEncoder(rw) + + rw.WriteHeader(http.StatusOK) + _ = enc.Encode(expected) + })) + defer mockAPI.Close() + + prov := oauth2.Heroku{} + tt, err := NewTestTripper(mockAPI, http.DefaultTransport) + if err != nil { + t.Fatal("Error initializing TestTripper: err:", err) + } + + tc := &http.Client{ + Transport: tt, + } + + email, err := prov.PrincipalID(tc) + if err != nil { + t.Fatal("Unexpected error while retrieiving PrincipalID: err:", err) + } + + if email != expected.Email { + t.Fatal("Retrieved email was not as expected. Want:", expected.Email, "Got:", email) + } +}