From d0299d614ec18e26e6fb33f95026315526590987 Mon Sep 17 00:00:00 2001 From: Florin Peter Date: Sun, 2 Apr 2017 19:50:33 +0200 Subject: [PATCH 1/5] added keycloak provider --- providers/keycloak.go | 58 +++++++++++++++++ providers/keycloak_test.go | 128 +++++++++++++++++++++++++++++++++++++ providers/providers.go | 2 + 3 files changed, 188 insertions(+) create mode 100644 providers/keycloak.go create mode 100644 providers/keycloak_test.go diff --git a/providers/keycloak.go b/providers/keycloak.go new file mode 100644 index 000000000..668bb524d --- /dev/null +++ b/providers/keycloak.go @@ -0,0 +1,58 @@ +package providers + +import ( + "log" + "net/http" + "net/url" + + "github.com/bitly/oauth2_proxy/api" +) + +type KeycloakProvider struct { + *ProviderData +} + +func NewKeycloakProvider(p *ProviderData) *KeycloakProvider { + p.ProviderName = "Keycloak" + if p.LoginURL == nil || p.LoginURL.String() == "" { + p.LoginURL = &url.URL{ + Scheme: "https", + Host: "keycloak.org", + Path: "/oauth/authorize", + } + } + if p.RedeemURL == nil || p.RedeemURL.String() == "" { + p.RedeemURL = &url.URL{ + Scheme: "https", + Host: "keycloak.org", + Path: "/oauth/token", + } + } + if p.ValidateURL == nil || p.ValidateURL.String() == "" { + p.ValidateURL = &url.URL{ + Scheme: "https", + Host: "keycloak.org", + Path: "/api/v3/user", + } + } + if p.Scope == "" { + p.Scope = "api" + } + return &KeycloakProvider{ProviderData: p} +} + +func (p *KeycloakProvider) GetEmailAddress(s *SessionState) (string, error) { + + req, err := http.NewRequest("GET", p.ValidateURL.String(), nil) + req.Header.Set("Authorization:", "Bearer "+s.AccessToken) + if err != nil { + log.Printf("failed building request %s", err) + return "", err + } + json, err := api.Request(req) + if err != nil { + log.Printf("failed making request %s", err) + return "", err + } + return json.Get("email").String() +} diff --git a/providers/keycloak_test.go b/providers/keycloak_test.go new file mode 100644 index 000000000..a795cad56 --- /dev/null +++ b/providers/keycloak_test.go @@ -0,0 +1,128 @@ +package providers + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/bmizerany/assert" +) + +func testKeycloakProvider(hostname string) *KeycloakProvider { + p := NewKeycloakProvider( + &ProviderData{ + ProviderName: "", + LoginURL: &url.URL{}, + RedeemURL: &url.URL{}, + ProfileURL: &url.URL{}, + ValidateURL: &url.URL{}, + Scope: ""}) + 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 testKeycloakBackend(payload string) *httptest.Server { + path := "/api/v3/user" + query := "access_token=imaginary_access_token" + + return httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + url := r.URL + if url.Path != path || url.RawQuery != query { + w.WriteHeader(404) + } else { + w.WriteHeader(200) + w.Write([]byte(payload)) + } + })) +} + +func TestKeycloakProviderDefaults(t *testing.T) { + p := testKeycloakProvider("") + assert.NotEqual(t, nil, p) + assert.Equal(t, "Keycloak", p.Data().ProviderName) + assert.Equal(t, "https://keycloak.org/oauth/authorize", + p.Data().LoginURL.String()) + assert.Equal(t, "https://keycloak.org/oauth/token", + p.Data().RedeemURL.String()) + assert.Equal(t, "https://keycloak.org/api/v3/user", + p.Data().ValidateURL.String()) + assert.Equal(t, "api", p.Data().Scope) +} + +func TestKeycloakProviderOverrides(t *testing.T) { + p := NewKeycloakProvider( + &ProviderData{ + LoginURL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/auth"}, + RedeemURL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/token"}, + ValidateURL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/api/v3/user"}, + Scope: "profile"}) + assert.NotEqual(t, nil, p) + assert.Equal(t, "Keycloak", 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/api/v3/user", + p.Data().ValidateURL.String()) + assert.Equal(t, "profile", p.Data().Scope) +} + +func TestKeycloakProviderGetEmailAddress(t *testing.T) { + b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\"}") + defer b.Close() + + b_url, _ := url.Parse(b.URL) + p := testKeycloakProvider(b_url.Host) + + session := &SessionState{AccessToken: "imaginary_access_token"} + email, err := p.GetEmailAddress(session) + assert.Equal(t, nil, err) + assert.Equal(t, "michael.bland@gsa.gov", email) +} + +// Note that trying to trigger the "failed building request" case is not +// practical, since the only way it can fail is if the URL fails to parse. +func TestKeycloakProviderGetEmailAddressFailedRequest(t *testing.T) { + b := testKeycloakBackend("unused payload") + defer b.Close() + + b_url, _ := url.Parse(b.URL) + p := testKeycloakProvider(b_url.Host) + + // We'll trigger a request failure by using an unexpected access + // token. Alternatively, we could allow the parsing of the payload as + // JSON to fail. + session := &SessionState{AccessToken: "unexpected_access_token"} + email, err := p.GetEmailAddress(session) + assert.NotEqual(t, nil, err) + assert.Equal(t, "", email) +} + +func TestKeycloakProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) { + b := testKeycloakBackend("{\"foo\": \"bar\"}") + defer b.Close() + + b_url, _ := url.Parse(b.URL) + p := testKeycloakProvider(b_url.Host) + + session := &SessionState{AccessToken: "imaginary_access_token"} + email, err := p.GetEmailAddress(session) + assert.NotEqual(t, nil, err) + assert.Equal(t, "", email) +} diff --git a/providers/providers.go b/providers/providers.go index fb2e5fc51..d8c9271eb 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -30,6 +30,8 @@ func New(provider string, p *ProviderData) Provider { return NewAzureProvider(p) case "gitlab": return NewGitLabProvider(p) + case "keycloak": + return NewKeycloakProvider(p) default: return NewGoogleProvider(p) } From f1ef8a81d360895645f77acfe3f74ea385933ee4 Mon Sep 17 00:00:00 2001 From: Florin Peter Date: Sun, 2 Apr 2017 19:59:03 +0200 Subject: [PATCH 2/5] improved tests --- providers/keycloak.go | 2 +- providers/keycloak_test.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/providers/keycloak.go b/providers/keycloak.go index 668bb524d..33292dc4a 100644 --- a/providers/keycloak.go +++ b/providers/keycloak.go @@ -44,7 +44,7 @@ func NewKeycloakProvider(p *ProviderData) *KeycloakProvider { func (p *KeycloakProvider) GetEmailAddress(s *SessionState) (string, error) { req, err := http.NewRequest("GET", p.ValidateURL.String(), nil) - req.Header.Set("Authorization:", "Bearer "+s.AccessToken) + req.Header.Set("Authorization", "Bearer "+s.AccessToken) if err != nil { log.Printf("failed building request %s", err) return "", err diff --git a/providers/keycloak_test.go b/providers/keycloak_test.go index a795cad56..1d95000b0 100644 --- a/providers/keycloak_test.go +++ b/providers/keycloak_test.go @@ -29,13 +29,14 @@ func testKeycloakProvider(hostname string) *KeycloakProvider { func testKeycloakBackend(payload string) *httptest.Server { path := "/api/v3/user" - query := "access_token=imaginary_access_token" return httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { url := r.URL - if url.Path != path || url.RawQuery != query { + 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)) From 264230ec0a3dea03cfdfb0a15fc008866ad75f41 Mon Sep 17 00:00:00 2001 From: Florin Peter Date: Sun, 2 Apr 2017 20:16:57 +0200 Subject: [PATCH 3/5] fixed indents --- providers/keycloak_test.go | 2 +- providers/providers.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/keycloak_test.go b/providers/keycloak_test.go index 1d95000b0..c994ee915 100644 --- a/providers/keycloak_test.go +++ b/providers/keycloak_test.go @@ -36,7 +36,7 @@ func testKeycloakBackend(payload string) *httptest.Server { if url.Path != path { w.WriteHeader(404) } else if r.Header.Get("Authorization") != "Bearer imaginary_access_token" { - w.WriteHeader(403) + w.WriteHeader(403) } else { w.WriteHeader(200) w.Write([]byte(payload)) diff --git a/providers/providers.go b/providers/providers.go index d8c9271eb..06832ac3d 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -31,7 +31,7 @@ func New(provider string, p *ProviderData) Provider { case "gitlab": return NewGitLabProvider(p) case "keycloak": - return NewKeycloakProvider(p) + return NewKeycloakProvider(p) default: return NewGoogleProvider(p) } From d170e9c8bc5c5be62f95ec683f9d67ecf8c65b25 Mon Sep 17 00:00:00 2001 From: Florin Peter Date: Tue, 4 Apr 2017 23:12:37 +0200 Subject: [PATCH 4/5] added keycloak group --- options.go | 3 +++ providers/keycloak.go | 27 +++++++++++++++++++++++++++ providers/keycloak_test.go | 28 +++++++++++++++++++++++----- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/options.go b/options.go index 63f23c6c7..1c2efc739 100644 --- a/options.go +++ b/options.go @@ -28,6 +28,7 @@ type Options struct { TLSKeyFile string `flag:"tls-key" cfg:"tls_key_file"` AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"` + KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group"` AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"` EmailDomains []string `flag:"email-domain" cfg:"email_domains"` GitHubOrg string `flag:"github-org" cfg:"github_org"` @@ -241,6 +242,8 @@ func parseProviderInfo(o *Options, msgs []string) []string { p.Configure(o.AzureTenant) case *providers.GitHubProvider: p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam) + case *providers.KeycloakProvider: + p.SetGroup(o.KeycloakGroup) case *providers.GoogleProvider: if o.GoogleServiceAccountJSON != "" { file, err := os.Open(o.GoogleServiceAccountJSON) diff --git a/providers/keycloak.go b/providers/keycloak.go index 33292dc4a..c7ef2e2fa 100644 --- a/providers/keycloak.go +++ b/providers/keycloak.go @@ -10,6 +10,7 @@ import ( type KeycloakProvider struct { *ProviderData + Group string } func NewKeycloakProvider(p *ProviderData) *KeycloakProvider { @@ -41,6 +42,10 @@ func NewKeycloakProvider(p *ProviderData) *KeycloakProvider { return &KeycloakProvider{ProviderData: p} } +func (p *KeycloakProvider) SetGroup(group string) { + p.Group = group +} + func (p *KeycloakProvider) GetEmailAddress(s *SessionState) (string, error) { req, err := http.NewRequest("GET", p.ValidateURL.String(), nil) @@ -54,5 +59,27 @@ func (p *KeycloakProvider) GetEmailAddress(s *SessionState) (string, error) { log.Printf("failed making request %s", err) return "", err } + + if p.Group != "" { + var groups, err = json.Get("groups").Array() + if err != nil { + log.Printf("groups not found %s", err) + return "", err + } + + var found = false + for i := range groups { + if groups[i].(string) == p.Group { + found = true + break + } + } + + if found != true { + log.Printf("group not found, access denied") + return "", nil + } + } + return json.Get("email").String() } diff --git a/providers/keycloak_test.go b/providers/keycloak_test.go index c994ee915..b5b078bf1 100644 --- a/providers/keycloak_test.go +++ b/providers/keycloak_test.go @@ -9,7 +9,7 @@ import ( "github.com/bmizerany/assert" ) -func testKeycloakProvider(hostname string) *KeycloakProvider { +func testKeycloakProvider(hostname, group string) *KeycloakProvider { p := NewKeycloakProvider( &ProviderData{ ProviderName: "", @@ -18,6 +18,11 @@ func testKeycloakProvider(hostname string) *KeycloakProvider { ProfileURL: &url.URL{}, ValidateURL: &url.URL{}, Scope: ""}) + + if group != "" { + p.SetGroup(group) + } + if hostname != "" { updateURL(p.Data().LoginURL, hostname) updateURL(p.Data().RedeemURL, hostname) @@ -45,7 +50,7 @@ func testKeycloakBackend(payload string) *httptest.Server { } func TestKeycloakProviderDefaults(t *testing.T) { - p := testKeycloakProvider("") + p := testKeycloakProvider("", "") assert.NotEqual(t, nil, p) assert.Equal(t, "Keycloak", p.Data().ProviderName) assert.Equal(t, "https://keycloak.org/oauth/authorize", @@ -89,7 +94,20 @@ func TestKeycloakProviderGetEmailAddress(t *testing.T) { defer b.Close() b_url, _ := url.Parse(b.URL) - p := testKeycloakProvider(b_url.Host) + p := testKeycloakProvider(b_url.Host, "") + + session := &SessionState{AccessToken: "imaginary_access_token"} + email, err := p.GetEmailAddress(session) + assert.Equal(t, nil, err) + assert.Equal(t, "michael.bland@gsa.gov", email) +} + +func TestKeycloakProviderGetEmailAddressAndGroup(t *testing.T) { + b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\", \"groups\": [\"test-grp1\", \"test-grp2\"]}") + defer b.Close() + + b_url, _ := url.Parse(b.URL) + p := testKeycloakProvider(b_url.Host, "test-grp1") session := &SessionState{AccessToken: "imaginary_access_token"} email, err := p.GetEmailAddress(session) @@ -104,7 +122,7 @@ func TestKeycloakProviderGetEmailAddressFailedRequest(t *testing.T) { defer b.Close() b_url, _ := url.Parse(b.URL) - p := testKeycloakProvider(b_url.Host) + p := testKeycloakProvider(b_url.Host, "") // We'll trigger a request failure by using an unexpected access // token. Alternatively, we could allow the parsing of the payload as @@ -120,7 +138,7 @@ func TestKeycloakProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) { defer b.Close() b_url, _ := url.Parse(b.URL) - p := testKeycloakProvider(b_url.Host) + p := testKeycloakProvider(b_url.Host, "") session := &SessionState{AccessToken: "imaginary_access_token"} email, err := p.GetEmailAddress(session) From 9db3f09a247e5fbd92f26ab3b692d298f16ddc36 Mon Sep 17 00:00:00 2001 From: Florin Peter Date: Tue, 4 Apr 2017 23:49:44 +0200 Subject: [PATCH 5/5] added missing flag --- main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/main.go b/main.go index b5ed971e6..30aa07314 100644 --- a/main.go +++ b/main.go @@ -42,6 +42,7 @@ func main() { flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS") flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email") + flagSet.String("keycloak-group", "", "restrict login to members of this group.") 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")