From 2e3a5fc81cd49df6859d92b6ce1ffac431b3b83d Mon Sep 17 00:00:00 2001 From: archielc Date: Mon, 4 Apr 2016 16:51:04 +0300 Subject: [PATCH] Implemented Zendesk provider support --- README.md | 9 +++ main.go | 1 + options.go | 3 + providers/providers.go | 2 + providers/zendesk.go | 89 +++++++++++++++++++++++++++++ providers/zendesk_test.go | 114 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 218 insertions(+) create mode 100644 providers/zendesk.go create mode 100644 providers/zendesk_test.go diff --git a/README.md b/README.md index c05f690c0..a164ebe6f 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Valid providers are : * [GitLab](#gitlab-auth-provider) * [LinkedIn](#linkedin-auth-provider) * [MyUSA](#myusa-auth-provider) +* [Zendesk](#zendesk-auth-provider) The provider can be selected using the `provider` configuration value. @@ -130,6 +131,13 @@ For LinkedIn, the registration steps are: The [MyUSA](https://alpha.my.usa.gov) authentication service ([GitHub](https://github.com/18F/myusa)) +### Zendesk Auth Provider + +1. Follow these steps to register Zendesk OAuth2 application: [Register your application with Zendesk](https://support.zendesk.com/hc/en-us/articles/203663836-Using-OAuth-authentication-with-your-application#topic_s21_lfs_qk). +2. For "Redirect URLs", provide `https://internal.yourcompany.com/oauth2/callback`. +3. Provide **Unique Identifier** (passed as `--client-id`) and take note of **Secret** (passed as `--client-secret`). +3. Provide your subdomain via the `--zendesk-subdomain=` option. + ### Microsoft Azure AD Provider For adding an application to the Microsoft Azure AD follow [these steps to add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/). @@ -196,6 +204,7 @@ Usage of oauth2_proxy: -upstream=: the http url(s) of the upstream endpoint or file:// paths for static files. Routing is based on the path -validate-url="": Access token validation endpoint -version=false: print version string + -zendesk-subodmain="": subdomain for Zendesk ``` See below for provider specific options diff --git a/main.go b/main.go index dd9a100e8..a158ba007 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,7 @@ func main() { flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.") flagSet.String("github-org", "", "restrict logins to members of this organisation") flagSet.String("github-team", "", "restrict logins to members of this team") + flagSet.String("zendesk-subdomain", "", "subdomain for Zendesk") flagSet.Var(&googleGroups, "google-group", "restrict logins to members of this google group (may be given multiple times).") flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls") flagSet.String("google-service-account-json", "", "the path to the service account json credentials") diff --git a/options.go b/options.go index 37e66b4c4..8f192d5c6 100644 --- a/options.go +++ b/options.go @@ -32,6 +32,7 @@ type Options struct { GoogleGroups []string `flag:"google-group" cfg:"google_group"` GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"` GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"` + ZendeskSubdomain string `flag:"zendesk-subdomain" cfg:"zendesk_subdomain"` HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"` DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form"` CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir"` @@ -215,6 +216,8 @@ func parseProviderInfo(o *Options, msgs []string) []string { p.Configure(o.AzureTenant) case *providers.GitHubProvider: p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam) + case *providers.ZendeskProvider: + p.Configure(o.ZendeskSubdomain) case *providers.GoogleProvider: if o.GoogleServiceAccountJSON != "" { file, err := os.Open(o.GoogleServiceAccountJSON) diff --git a/providers/providers.go b/providers/providers.go index 010e633bf..897c9c576 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -28,6 +28,8 @@ func New(provider string, p *ProviderData) Provider { return NewAzureProvider(p) case "gitlab": return NewGitLabProvider(p) + case "zendesk": + return NewZendeskProvider(p) default: return NewGoogleProvider(p) } diff --git a/providers/zendesk.go b/providers/zendesk.go new file mode 100644 index 000000000..2ea8623ee --- /dev/null +++ b/providers/zendesk.go @@ -0,0 +1,89 @@ +package providers + +import ( + "errors" + "fmt" + "github.com/bitly/oauth2_proxy/api" + "log" + "net/http" + "net/url" +) + +type ZendeskProvider struct { + *ProviderData + Subdomain string +} + +func NewZendeskProvider(p *ProviderData) *ZendeskProvider { + p.ProviderName = "Zendesk" + + if p.Scope == "" { + p.Scope = "read" + } + + return &ZendeskProvider{ProviderData: p} +} + +func (p *ZendeskProvider) Configure(subdomain string) { + p.Subdomain = subdomain + + if p.LoginURL == nil || p.LoginURL.String() == "" { + p.LoginURL = &url.URL{ + Scheme: "https", + Host: p.Subdomain + ".zendesk.com", + Path: "/oauth/authorizations/new"} + } + + if p.ProfileURL == nil || p.ProfileURL.String() == "" { + p.ProfileURL = &url.URL{ + Scheme: "https", + Host: p.Subdomain + ".zendesk.com", + Path: "/api/v2/users/me.json"} + } + + if p.RedeemURL == nil || p.RedeemURL.String() == "" { + p.RedeemURL = &url.URL{ + Scheme: "https", + Host: p.Subdomain + ".zendesk.com", + Path: "/oauth/tokens"} + } + + if p.ProtectedResource == nil || p.ProtectedResource.String() == "" { + p.ProtectedResource = &url.URL{ + Scheme: "https", + Host: p.Subdomain + ".zendesk.com", + } + } +} + +func getZendeskHeader(access_token string) http.Header { + header := make(http.Header) + header.Set("Authorization", fmt.Sprintf("Bearer %s", access_token)) + return header +} + +func (p *ZendeskProvider) GetEmailAddress(s *SessionState) (string, error) { + if s.AccessToken == "" { + return "", errors.New("missing access token") + } + req, err := http.NewRequest("GET", p.ProfileURL.String(), nil) + if err != nil { + return "", err + } + req.Header = getAzureHeader(s.AccessToken) + + json, err := api.Request(req) + + if err != nil { + log.Printf("failed making request %s", err) + return "", err + } + + email, err := json.Get("user").Get("email").String() + if err != nil { + fmt.Printf("failed parsing JSON '%s'; error %s", email, err) + return "", err + } + + return email, nil +} diff --git a/providers/zendesk_test.go b/providers/zendesk_test.go new file mode 100644 index 000000000..0f50e351a --- /dev/null +++ b/providers/zendesk_test.go @@ -0,0 +1,114 @@ +package providers + +import ( + "github.com/bmizerany/assert" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +func testZendeskProvider(hostname string) *ZendeskProvider { + p := NewZendeskProvider( + &ProviderData{ + ProviderName: "", + LoginURL: &url.URL{}, + RedeemURL: &url.URL{}, + ProfileURL: &url.URL{}, + ValidateURL: &url.URL{}, + Scope: ""}) + p.Configure("example") + if hostname != "" { + updateURL(p.Data().LoginURL, hostname) + updateURL(p.Data().RedeemURL, hostname) + updateURL(p.Data().ProfileURL, hostname) + updateURL(p.Data().ValidateURL, hostname) + } + return p +} + +func TestZendeskProviderOverrides(t *testing.T) { + p := NewZendeskProvider( + &ProviderData{ + LoginURL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/auth"}, + RedeemURL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/token"}, + ProfileURL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/profile"}, + ValidateURL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/tokeninfo"}, + ProtectedResource: &url.URL{ + Scheme: "https", + Host: "example.com"}, + Scope: "profile"}) + assert.NotEqual(t, nil, p) + assert.Equal(t, "Zendesk", p.Data().ProviderName) + assert.Equal(t, "https://example.com/oauth/auth", + p.Data().LoginURL.String()) + assert.Equal(t, "https://example.com/oauth/token", + p.Data().RedeemURL.String()) + assert.Equal(t, "https://example.com/oauth/profile", + p.Data().ProfileURL.String()) + assert.Equal(t, "https://example.com/oauth/tokeninfo", + p.Data().ValidateURL.String()) + assert.Equal(t, "https://example.com", + p.Data().ProtectedResource.String()) + assert.Equal(t, "profile", p.Data().Scope) +} + +func TestZendeskSetSubdomain(t *testing.T) { + p := testZendeskProvider("") + p.Configure("example") + assert.Equal(t, "Zendesk", p.Data().ProviderName) + assert.Equal(t, "example", p.Subdomain) + assert.Equal(t, "https://example.zendesk.com/oauth/authorizations/new", + p.Data().LoginURL.String()) + assert.Equal(t, "https://example.zendesk.com/oauth/tokens", + p.Data().RedeemURL.String()) + assert.Equal(t, "https://example.zendesk.com/api/v2/users/me.json", + p.Data().ProfileURL.String()) + assert.Equal(t, "https://example.zendesk.com", + p.Data().ProtectedResource.String()) + assert.Equal(t, "", + p.Data().ValidateURL.String()) + assert.Equal(t, "read", p.Data().Scope) +} + +func testZendeskBackend(payload string) *httptest.Server { + path := "/api/v2/users/me.json" + + return httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + url := r.URL + if url.Path != path { + w.WriteHeader(404) + } else if r.Header.Get("Authorization") != "Bearer imaginary_access_token" { + w.WriteHeader(403) + } else { + w.WriteHeader(200) + w.Write([]byte(payload)) + } + })) +} + +func TestZendeskProviderGetEmailAddress(t *testing.T) { + b := testZendeskBackend(`{"user": {"id":5383137307,"url":"https://example.zendesk.com/api/v2/end_users/5555555555.json","name":"Zensdesk Test","email":"zendesk.test@example.com","created_at":"2016-03-31T14:51:17Z","updated_at":"2016-04-04T09:05:00Z","time_zone":"Eastern Time (US & Canada)","phone":null,"photo":null,"locale_id":1,"locale":"en-US","organization_id":null,"role":"end-user","verified":true}}`) + defer b.Close() + + b_url, _ := url.Parse(b.URL) + p := testZendeskProvider(b_url.Host) + + session := &SessionState{AccessToken: "imaginary_access_token"} + email, err := p.GetEmailAddress(session) + assert.Equal(t, nil, err) + assert.Equal(t, "zendesk.test@example.com", email) +}