git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge branch 'master' into kamal/whitelist-redirects-with-ports
commit11cc396059440802e53daf5847abc69c1525defd authorDan Bond <pm@danbond.io> dateWed 15 Jan 2020, 14:03 (7 years ago) parent625909eeee4b55
7 files changed+241−1
.github/CODEOWNERS+4 CHANGELOG.md+1 contrib/oauth2_proxy_autocomplete.sh+1−1 docs/2_auth.md+18 providers/digitalocean.go+81 providers/digitalocean_test.go+134 providers/providers.go+2
.github/CODEOWNERS+4
@@ -18,3 +18,7 @@ providers/bitbucket_test.go @aledeganopix4d
1818# Nextcloud provider
1919providers/nextcloud.go @Ramblurr
2020providers/nextcloud_test.go @Ramblurr
21+
22+# DigitalOcean provider
23+providers/digitalocean.go @kamaln7
24+providers/digitalocean_test.go @kamaln7
CHANGELOG.md+1
@@ -9,6 +9,7 @@
99- [#325](https://github.com/pusher/oauth2_proxy/pull/325) dist.sh: use sha256sum (@syscll)
1010- [#179](https://github.com/pusher/oauth2_proxy/pull/179) Add Nextcloud provider (@Ramblurr)
1111- [#280](https://github.com/pusher/oauth2_proxy/pull/280) whitelisted redirect domains: add support for whitelisting specific ports or allowing wildcard ports (@kamaln7)
12+- [#351](https://github.com/pusher/oauth2_proxy/pull/351) Add DigitalOcean Auth provider (@kamaln7)
1213
1314# v4.1.0
1415
contrib/oauth2_proxy_autocomplete.sh+1−1
@@ -17,7 +17,7 @@ _oauth2_proxy() {
1717 return 0
1818 ;;
1919 -provider)
20− COMPREPLY=( $(compgen -W "google azure facebook github keycloak gitlab linkedin login.gov" -- ${cur}) )
20+ COMPREPLY=( $(compgen -W "google azure facebook github keycloak gitlab linkedin login.gov digitalocean" -- ${cur}) )
2121 return 0
2222 ;;
2323 -@(http-address|https-address|redirect-url|upstream|basic-auth-password|skip-auth-regex|flush-interval|extra-jwt-issuers|email-domain|whitelist-domain|keycloak-group|azure-tenant|bitbucket-team|bitbucket-repository|github-org|github-team|gitlab-group|google-group|google-admin-email|google-service-account-json|client-id|client_secret|banner|footer|proxy-prefix|ping-path|cookie-name|cookie-secret|cookie-domain|cookie-path|cookie-expire|cookie-refresh|redist-sentinel-master-name|redist-sentinel-connection-urls|logging-max-size|logging-max-age|logging-max-backups|standard-logging-format|request-logging-format|exclude-logging-paths|auth-logging-format|oidc-issuer-url|oidc-jwks-url|login-url|redeem-url|profile-url|resource|validate-url|scope|approval-prompt|signature-key|acr-values|jwt-key|pubjwk-url))
docs/2_auth.md+18
@@ -20,6 +20,7 @@ Valid providers are :
2020- [LinkedIn](#linkedin-auth-provider)
2121- [login.gov](#logingov-provider)
2222- [Nextcloud](#nextcloud-provider)
23+- [DigitalOcean](#digitalocean-auth-provider)
2324
2425The provider can be selected using the `provider` configuration value.
2526
@@ -320,6 +321,23 @@ to setup the client id and client secret. Your "Redirection URI" will be
320321
321322Note: in *all* cases the validate-url will *not* have the `index.php`.
322323
324+### DigitalOcean Auth Provider
325+
326+1. [Create a new OAuth application](https://cloud.digitalocean.com/account/api/applications)
327+ * You can fill in the name, homepage, and description however you wish.
328+ * In the "Application callback URL" field, enter: `https://oauth-proxy/oauth2/callback`, substituting `oauth2-proxy` with the actual hostname that oauth2_proxy is running on. The URL must match oauth2_proxy's configured redirect URL.
329+2. Note the Client ID and Client Secret.
330+
331+To use the provider, pass the following options:
332+
333+```
334+ --provider=digitalocean
335+ --client-id=<Client ID>
336+ --client-secret=<Client Secret>
337+```
338+
339+ Alternatively, set the equivalent options in the config file. The redirect URL defaults to `https://<requested host header>/oauth2/callback`. If you need to change it, you can use the `--redirect-url` command-line option.
340+
323341## Email Authentication
324342
325343To authorize by email domain use `--email-domain=yourcompany.com`. To authorize individual email addresses use `--authenticated-emails-file=/path/to/file` with one email per line. To authorize all email addresses use `--email-domain=*`.
providers/digitalocean.gonew file+81
@@ -0,0 +1,81 @@
1+package providers
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "net/http"
7+ "net/url"
8+
9+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
10+ "github.com/pusher/oauth2_proxy/pkg/requests"
11+)
12+
13+// DigitalOceanProvider represents a DigitalOcean based Identity Provider
14+type DigitalOceanProvider struct {
15+ *ProviderData
16+}
17+
18+// NewDigitalOceanProvider initiates a new DigitalOceanProvider
19+func NewDigitalOceanProvider(p *ProviderData) *DigitalOceanProvider {
20+ p.ProviderName = "DigitalOcean"
21+ if p.LoginURL.String() == "" {
22+ p.LoginURL = &url.URL{Scheme: "https",
23+ Host: "cloud.digitalocean.com",
24+ Path: "/v1/oauth/authorize",
25+ }
26+ }
27+ if p.RedeemURL.String() == "" {
28+ p.RedeemURL = &url.URL{Scheme: "https",
29+ Host: "cloud.digitalocean.com",
30+ Path: "/v1/oauth/token",
31+ }
32+ }
33+ if p.ProfileURL.String() == "" {
34+ p.ProfileURL = &url.URL{Scheme: "https",
35+ Host: "api.digitalocean.com",
36+ Path: "/v2/account",
37+ }
38+ }
39+ if p.ValidateURL.String() == "" {
40+ p.ValidateURL = p.ProfileURL
41+ }
42+ if p.Scope == "" {
43+ p.Scope = "read"
44+ }
45+ return &DigitalOceanProvider{ProviderData: p}
46+}
47+
48+func getDigitalOceanHeader(accessToken string) http.Header {
49+ header := make(http.Header)
50+ header.Set("Content-Type", "application/json")
51+ header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
52+ return header
53+}
54+
55+// GetEmailAddress returns the Account email address
56+func (p *DigitalOceanProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
57+ if s.AccessToken == "" {
58+ return "", errors.New("missing access token")
59+ }
60+ req, err := http.NewRequest("GET", p.ProfileURL.String(), nil)
61+ if err != nil {
62+ return "", err
63+ }
64+ req.Header = getDigitalOceanHeader(s.AccessToken)
65+
66+ json, err := requests.Request(req)
67+ if err != nil {
68+ return "", err
69+ }
70+
71+ email, err := json.GetPath("account", "email").String()
72+ if err != nil {
73+ return "", err
74+ }
75+ return email, nil
76+}
77+
78+// ValidateSessionState validates the AccessToken
79+func (p *DigitalOceanProvider) ValidateSessionState(s *sessions.SessionState) bool {
80+ return validateToken(p, s.AccessToken, getDigitalOceanHeader(s.AccessToken))
81+}
providers/digitalocean_test.gonew file+134
@@ -0,0 +1,134 @@
1+package providers
2+
3+import (
4+ "net/http"
5+ "net/http/httptest"
6+ "net/url"
7+ "testing"
8+
9+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
10+ "github.com/stretchr/testify/assert"
11+)
12+
13+func testDigitalOceanProvider(hostname string) *DigitalOceanProvider {
14+ p := NewDigitalOceanProvider(
15+ &ProviderData{
16+ ProviderName: "",
17+ LoginURL: &url.URL{},
18+ RedeemURL: &url.URL{},
19+ ProfileURL: &url.URL{},
20+ ValidateURL: &url.URL{},
21+ Scope: ""})
22+ if hostname != "" {
23+ updateURL(p.Data().LoginURL, hostname)
24+ updateURL(p.Data().RedeemURL, hostname)
25+ updateURL(p.Data().ProfileURL, hostname)
26+ }
27+ return p
28+}
29+
30+func testDigitalOceanBackend(payload string) *httptest.Server {
31+ path := "/v2/account"
32+
33+ return httptest.NewServer(http.HandlerFunc(
34+ func(w http.ResponseWriter, r *http.Request) {
35+ if r.URL.Path != path {
36+ w.WriteHeader(404)
37+ } else if r.Header.Get("Authorization") != "Bearer imaginary_access_token" {
38+ w.WriteHeader(403)
39+ } else {
40+ w.WriteHeader(200)
41+ w.Write([]byte(payload))
42+ }
43+ }))
44+}
45+
46+func TestDigitalOceanProviderDefaults(t *testing.T) {
47+ p := testDigitalOceanProvider("")
48+ assert.NotEqual(t, nil, p)
49+ assert.Equal(t, "DigitalOcean", p.Data().ProviderName)
50+ assert.Equal(t, "https://cloud.digitalocean.com/v1/oauth/authorize",
51+ p.Data().LoginURL.String())
52+ assert.Equal(t, "https://cloud.digitalocean.com/v1/oauth/token",
53+ p.Data().RedeemURL.String())
54+ assert.Equal(t, "https://api.digitalocean.com/v2/account",
55+ p.Data().ProfileURL.String())
56+ assert.Equal(t, "https://api.digitalocean.com/v2/account",
57+ p.Data().ValidateURL.String())
58+ assert.Equal(t, "read", p.Data().Scope)
59+}
60+
61+func TestDigitalOceanProviderOverrides(t *testing.T) {
62+ p := NewDigitalOceanProvider(
63+ &ProviderData{
64+ LoginURL: &url.URL{
65+ Scheme: "https",
66+ Host: "example.com",
67+ Path: "/oauth/auth"},
68+ RedeemURL: &url.URL{
69+ Scheme: "https",
70+ Host: "example.com",
71+ Path: "/oauth/token"},
72+ ProfileURL: &url.URL{
73+ Scheme: "https",
74+ Host: "example.com",
75+ Path: "/oauth/profile"},
76+ ValidateURL: &url.URL{
77+ Scheme: "https",
78+ Host: "example.com",
79+ Path: "/oauth/tokeninfo"},
80+ Scope: "profile"})
81+ assert.NotEqual(t, nil, p)
82+ assert.Equal(t, "DigitalOcean", p.Data().ProviderName)
83+ assert.Equal(t, "https://example.com/oauth/auth",
84+ p.Data().LoginURL.String())
85+ assert.Equal(t, "https://example.com/oauth/token",
86+ p.Data().RedeemURL.String())
87+ assert.Equal(t, "https://example.com/oauth/profile",
88+ p.Data().ProfileURL.String())
89+ assert.Equal(t, "https://example.com/oauth/tokeninfo",
90+ p.Data().ValidateURL.String())
91+ assert.Equal(t, "profile", p.Data().Scope)
92+}
93+
94+func TestDigitalOceanProviderGetEmailAddress(t *testing.T) {
95+ b := testDigitalOceanBackend(`{"account": {"email": "user@example.com"}}`)
96+ defer b.Close()
97+
98+ bURL, _ := url.Parse(b.URL)
99+ p := testDigitalOceanProvider(bURL.Host)
100+
101+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
102+ email, err := p.GetEmailAddress(session)
103+ assert.Equal(t, nil, err)
104+ assert.Equal(t, "user@example.com", email)
105+}
106+
107+func TestDigitalOceanProviderGetEmailAddressFailedRequest(t *testing.T) {
108+ b := testDigitalOceanBackend("unused payload")
109+ defer b.Close()
110+
111+ bURL, _ := url.Parse(b.URL)
112+ p := testDigitalOceanProvider(bURL.Host)
113+
114+ // We'll trigger a request failure by using an unexpected access
115+ // token. Alternatively, we could allow the parsing of the payload as
116+ // JSON to fail.
117+ session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
118+ email, err := p.GetEmailAddress(session)
119+ assert.NotEqual(t, nil, err)
120+ assert.Equal(t, "", email)
121+}
122+
123+func TestDigitalOceanProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
124+ b := testDigitalOceanBackend("{\"foo\": \"bar\"}")
125+ defer b.Close()
126+
127+ bURL, _ := url.Parse(b.URL)
128+ p := testDigitalOceanProvider(bURL.Host)
129+
130+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
131+ email, err := p.GetEmailAddress(session)
132+ assert.NotEqual(t, nil, err)
133+ assert.Equal(t, "", email)
134+}
providers/providers.go+2
@@ -42,6 +42,8 @@ func New(provider string, p *ProviderData) Provider {
4242 return NewBitbucketProvider(p)
4343 case "nextcloud":
4444 return NewNextcloudProvider(p)
45+ case "digitalocean":
46+ return NewDigitalOceanProvider(p)
4547 default:
4648 return NewGoogleProvider(p)
4749 }