Skip to content
This repository has been archived by the owner on Jan 24, 2019. It is now read-only.

Implemented Zendesk provider support #230

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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=<YOUR 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/).
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
3 changes: 3 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions providers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
89 changes: 89 additions & 0 deletions providers/zendesk.go
Original file line number Diff line number Diff line change
@@ -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
}
114 changes: 114 additions & 0 deletions providers/zendesk_test.go
Original file line number Diff line number Diff line change
@@ -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":"[email protected]","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, "[email protected]", email)
}