git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add keycloak provider
commit583ec18fa23e12b8c8b44ff870b754c3bc7b04a4 authorKarel Pokorny <Karel.Pokorny@konicaminolta.cz> dateSun 28 Jul 2019, 15:54 (7 years ago) parent3f219bd
5 files changed+240
main.go+1 options.go+3 providers/keycloak.go+86 providers/keycloak_test.go+148 providers/providers.go+2
main.go+1
@@ -54,6 +54,7 @@ func main() {
5454
5555 flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
5656 flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
57+ flagSet.String("keycloak-group", "", "restrict login to members of this group.")
5758 flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
5859 flagSet.String("github-org", "", "restrict logins to members of this organisation")
5960 flagSet.String("github-team", "", "restrict logins to members of this team")
options.go+3
@@ -41,6 +41,7 @@ type Options struct {
4141 TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"`
4242
4343 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"`
44+ KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group"`
4445 AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"`
4546 EmailDomains []string `flag:"email-domain" cfg:"email_domains" env:"OAUTH2_PROXY_EMAIL_DOMAINS"`
4647 WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains" env:"OAUTH2_PROXY_WHITELIST_DOMAINS"`
@@ -394,6 +395,8 @@ func parseProviderInfo(o *Options, msgs []string) []string {
394395 p.Configure(o.AzureTenant)
395396 case *providers.GitHubProvider:
396397 p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
398+ case *providers.KeycloakProvider:
399+ p.SetGroup(o.KeycloakGroup)
397400 case *providers.GoogleProvider:
398401 if o.GoogleServiceAccountJSON != "" {
399402 file, err := os.Open(o.GoogleServiceAccountJSON)
providers/keycloak.gonew file+86
@@ -0,0 +1,86 @@
1+package providers
2+
3+import (
4+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
5+ "log"
6+ "net/http"
7+ "net/url"
8+
9+ "github.com/bitly/oauth2_proxy/api"
10+)
11+
12+type KeycloakProvider struct {
13+ *ProviderData
14+ Group string
15+}
16+
17+func NewKeycloakProvider(p *ProviderData) *KeycloakProvider {
18+ p.ProviderName = "Keycloak"
19+ if p.LoginURL == nil || p.LoginURL.String() == "" {
20+ p.LoginURL = &url.URL{
21+ Scheme: "https",
22+ Host: "keycloak.org",
23+ Path: "/oauth/authorize",
24+ }
25+ }
26+ if p.RedeemURL == nil || p.RedeemURL.String() == "" {
27+ p.RedeemURL = &url.URL{
28+ Scheme: "https",
29+ Host: "keycloak.org",
30+ Path: "/oauth/token",
31+ }
32+ }
33+ if p.ValidateURL == nil || p.ValidateURL.String() == "" {
34+ p.ValidateURL = &url.URL{
35+ Scheme: "https",
36+ Host: "keycloak.org",
37+ Path: "/api/v3/user",
38+ }
39+ }
40+ if p.Scope == "" {
41+ p.Scope = "api"
42+ }
43+ return &KeycloakProvider{ProviderData: p}
44+}
45+
46+func (p *KeycloakProvider) SetGroup(group string) {
47+ p.Group = group
48+}
49+
50+func (p *KeycloakProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
51+
52+ req, err := http.NewRequest("GET", p.ValidateURL.String(), nil)
53+ req.Header.Set("Authorization", "Bearer "+s.AccessToken)
54+ if err != nil {
55+ log.Printf("failed building request %s", err)
56+ return "", err
57+ }
58+ json, err := api.Request(req)
59+ if err != nil {
60+ log.Printf("failed making request %s", err)
61+ return "", err
62+ }
63+
64+ if p.Group != "" {
65+ var groups, err = json.Get("groups").Array()
66+ if err != nil {
67+ log.Printf("groups not found %s", err)
68+ return "", err
69+ }
70+
71+ var found = false
72+ for i := range groups {
73+ if groups[i].(string) == p.Group {
74+ found = true
75+ break
76+ }
77+ }
78+
79+ if found != true {
80+ log.Printf("group not found, access denied")
81+ return "", nil
82+ }
83+ }
84+
85+ return json.Get("email").String()
86+}
providers/keycloak_test.gonew file+148
@@ -0,0 +1,148 @@
1+package providers
2+
3+import (
4+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
5+ "net/http"
6+ "net/http/httptest"
7+ "net/url"
8+ "testing"
9+
10+ "github.com/bmizerany/assert"
11+)
12+
13+func testKeycloakProvider(hostname, group string) *KeycloakProvider {
14+ p := NewKeycloakProvider(
15+ &ProviderData{
16+ ProviderName: "",
17+ LoginURL: &url.URL{},
18+ RedeemURL: &url.URL{},
19+ ProfileURL: &url.URL{},
20+ ValidateURL: &url.URL{},
21+ Scope: ""})
22+
23+ if group != "" {
24+ p.SetGroup(group)
25+ }
26+
27+ if hostname != "" {
28+ updateURL(p.Data().LoginURL, hostname)
29+ updateURL(p.Data().RedeemURL, hostname)
30+ updateURL(p.Data().ProfileURL, hostname)
31+ updateURL(p.Data().ValidateURL, hostname)
32+ }
33+ return p
34+}
35+
36+func testKeycloakBackend(payload string) *httptest.Server {
37+ path := "/api/v3/user"
38+
39+ return httptest.NewServer(http.HandlerFunc(
40+ func(w http.ResponseWriter, r *http.Request) {
41+ url := r.URL
42+ if url.Path != path {
43+ w.WriteHeader(404)
44+ } else if r.Header.Get("Authorization") != "Bearer imaginary_access_token" {
45+ w.WriteHeader(403)
46+ } else {
47+ w.WriteHeader(200)
48+ w.Write([]byte(payload))
49+ }
50+ }))
51+}
52+
53+func TestKeycloakProviderDefaults(t *testing.T) {
54+ p := testKeycloakProvider("", "")
55+ assert.NotEqual(t, nil, p)
56+ assert.Equal(t, "Keycloak", p.Data().ProviderName)
57+ assert.Equal(t, "https://keycloak.org/oauth/authorize",
58+ p.Data().LoginURL.String())
59+ assert.Equal(t, "https://keycloak.org/oauth/token",
60+ p.Data().RedeemURL.String())
61+ assert.Equal(t, "https://keycloak.org/api/v3/user",
62+ p.Data().ValidateURL.String())
63+ assert.Equal(t, "api", p.Data().Scope)
64+}
65+
66+func TestKeycloakProviderOverrides(t *testing.T) {
67+ p := NewKeycloakProvider(
68+ &ProviderData{
69+ LoginURL: &url.URL{
70+ Scheme: "https",
71+ Host: "example.com",
72+ Path: "/oauth/auth"},
73+ RedeemURL: &url.URL{
74+ Scheme: "https",
75+ Host: "example.com",
76+ Path: "/oauth/token"},
77+ ValidateURL: &url.URL{
78+ Scheme: "https",
79+ Host: "example.com",
80+ Path: "/api/v3/user"},
81+ Scope: "profile"})
82+ assert.NotEqual(t, nil, p)
83+ assert.Equal(t, "Keycloak", p.Data().ProviderName)
84+ assert.Equal(t, "https://example.com/oauth/auth",
85+ p.Data().LoginURL.String())
86+ assert.Equal(t, "https://example.com/oauth/token",
87+ p.Data().RedeemURL.String())
88+ assert.Equal(t, "https://example.com/api/v3/user",
89+ p.Data().ValidateURL.String())
90+ assert.Equal(t, "profile", p.Data().Scope)
91+}
92+
93+func TestKeycloakProviderGetEmailAddress(t *testing.T) {
94+ b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\"}")
95+ defer b.Close()
96+
97+ b_url, _ := url.Parse(b.URL)
98+ p := testKeycloakProvider(b_url.Host, "")
99+
100+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
101+ email, err := p.GetEmailAddress(session)
102+ assert.Equal(t, nil, err)
103+ assert.Equal(t, "michael.bland@gsa.gov", email)
104+}
105+
106+func TestKeycloakProviderGetEmailAddressAndGroup(t *testing.T) {
107+ b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\", \"groups\": [\"test-grp1\", \"test-grp2\"]}")
108+ defer b.Close()
109+
110+ b_url, _ := url.Parse(b.URL)
111+ p := testKeycloakProvider(b_url.Host, "test-grp1")
112+
113+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
114+ email, err := p.GetEmailAddress(session)
115+ assert.Equal(t, nil, err)
116+ assert.Equal(t, "michael.bland@gsa.gov", email)
117+}
118+
119+// Note that trying to trigger the "failed building request" case is not
120+// practical, since the only way it can fail is if the URL fails to parse.
121+func TestKeycloakProviderGetEmailAddressFailedRequest(t *testing.T) {
122+ b := testKeycloakBackend("unused payload")
123+ defer b.Close()
124+
125+ b_url, _ := url.Parse(b.URL)
126+ p := testKeycloakProvider(b_url.Host, "")
127+
128+ // We'll trigger a request failure by using an unexpected access
129+ // token. Alternatively, we could allow the parsing of the payload as
130+ // JSON to fail.
131+ session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
132+ email, err := p.GetEmailAddress(session)
133+ assert.NotEqual(t, nil, err)
134+ assert.Equal(t, "", email)
135+}
136+
137+func TestKeycloakProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
138+ b := testKeycloakBackend("{\"foo\": \"bar\"}")
139+ defer b.Close()
140+
141+ b_url, _ := url.Parse(b.URL)
142+ p := testKeycloakProvider(b_url.Host, "")
143+
144+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
145+ email, err := p.GetEmailAddress(session)
146+ assert.NotEqual(t, nil, err)
147+ assert.Equal(t, "", email)
148+}
providers/providers.go+2
@@ -28,6 +28,8 @@ func New(provider string, p *ProviderData) Provider {
2828 return NewFacebookProvider(p)
2929 case "github":
3030 return NewGitHubProvider(p)
31+ case "keycloak":
32+ return NewKeycloakProvider(p)
3133 case "azure":
3234 return NewAzureProvider(p)
3335 case "gitlab":