git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge branch 'master' into support-x-auth-request-redirect
commitac0d010371f4ad5642aa02bbffc0b7c1aca69112 authorJoel Speed <Joel.speed@hotmail.co.uk> dateMon 30 Sep 2019, 17:21 (7 years ago) parent809809444cdcc7
7 files changed+260
CHANGELOG.md+2 docs/2_auth.md+15 main.go+1 options.go+3 providers/keycloak.go+86 providers/keycloak_test.go+151 providers/providers.go+2
CHANGELOG.md+2
@@ -2,6 +2,8 @@
22
33## Changes since v4.0.0
44
5+- [#227](https://github.com/pusher/oauth2_proxy/pull/227) Add Keycloak provider (@Ofinka)
6+
57# v4.0.0
68
79- [#248](https://github.com/pusher/oauth2_proxy/pull/248) Fix issue with X-Auth-Request-Redirect header being ignored
docs/2_auth.md+15
@@ -15,6 +15,7 @@ Valid providers are :
1515- [Azure](#azure-auth-provider)
1616- [Facebook](#facebook-auth-provider)
1717- [GitHub](#github-auth-provider)
18+- [Keycloak](#keycloak-auth-provider)
1819- [GitLab](#gitlab-auth-provider)
1920- [LinkedIn](#linkedin-auth-provider)
2021- [login.gov](#logingov-provider)
@@ -101,6 +102,20 @@ If you are using GitHub enterprise, make sure you set the following to the appro
101102 -redeem-url="http(s)://<enterprise github host>/login/oauth/access_token"
102103 -validate-url="http(s)://<enterprise github host>/api/v3"
103104
105+### Keycloak Auth Provider
106+
107+1. Create new client in your Keycloak with **Access Type** 'confidental'.
108+2. Create a mapper with **Mapper Type** 'Group Membership'.
109+
110+Make sure you set the following to the appropriate url:
111+
112+ -provider=keycloak
113+ -client-id=<client you have created>
114+ -client-secret=<your client's secret>
115+ -login-url="http(s)://<keycloak host>/realms/<your realm>/protocol/openid-connect/auth"
116+ -redeem-url="http(s)://<keycloak host>/realms/master/<your realm>/openid-connect/auth/token"
117+ -validate-url="http(s)://<keycloak host>/realms/master/<your realm>/openid-connect/userinfo"
118+
104119### GitLab Auth Provider
105120
106121Whether you are using GitLab.com or self-hosting GitLab, follow [these steps to add an application](https://docs.gitlab.com/ce/integration/oauth_provider.html). Make sure to enable at least the `openid`, `profile` and `email` scopes.
main.go+1
@@ -55,6 +55,7 @@ func main() {
5555
5656 flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
5757 flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
58+ flagSet.String("keycloak-group", "", "restrict login to members of this group.")
5859 flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
5960 flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
6061 flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")
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" env:"OAUTH2_PROXY_KEYCLOAK_GROUP"`
4445 AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"`
4546 BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team" env:"OAUTH2_PROXY_BITBUCKET_TEAM"`
4647 BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository" env:"OAUTH2_PROXY_BITBUCKET_REPOSITORY"`
@@ -398,6 +399,8 @@ func parseProviderInfo(o *Options, msgs []string) []string {
398399 p.Configure(o.AzureTenant)
399400 case *providers.GitHubProvider:
400401 p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
402+ case *providers.KeycloakProvider:
403+ p.SetGroup(o.KeycloakGroup)
401404 case *providers.GoogleProvider:
402405 if o.GoogleServiceAccountJSON != "" {
403406 file, err := os.Open(o.GoogleServiceAccountJSON)
providers/keycloak.gonew file+86
@@ -0,0 +1,86 @@
1+package providers
2+
3+import (
4+ "net/http"
5+ "net/url"
6+
7+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
8+ "github.com/pusher/oauth2_proxy/pkg/logger"
9+ "github.com/pusher/oauth2_proxy/pkg/requests"
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+ logger.Printf("failed building request %s", err)
56+ return "", err
57+ }
58+ json, err := requests.Request(req)
59+ if err != nil {
60+ logger.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+ logger.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+ logger.Printf("group not found, access denied")
81+ return "", nil
82+ }
83+ }
84+
85+ return json.Get("email").String()
86+}
providers/keycloak_test.gonew file+151
@@ -0,0 +1,151 @@
1+package providers
2+
3+import (
4+ "net/http"
5+ "net/http/httptest"
6+ "net/url"
7+ "testing"
8+
9+ "github.com/bmizerany/assert"
10+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
11+)
12+
13+const imaginaryAccessToken = "imaginary_access_token"
14+const bearerAccessToken = "Bearer " + imaginaryAccessToken
15+
16+func testKeycloakProvider(hostname, group string) *KeycloakProvider {
17+ p := NewKeycloakProvider(
18+ &ProviderData{
19+ ProviderName: "",
20+ LoginURL: &url.URL{},
21+ RedeemURL: &url.URL{},
22+ ProfileURL: &url.URL{},
23+ ValidateURL: &url.URL{},
24+ Scope: ""})
25+
26+ if group != "" {
27+ p.SetGroup(group)
28+ }
29+
30+ if hostname != "" {
31+ updateURL(p.Data().LoginURL, hostname)
32+ updateURL(p.Data().RedeemURL, hostname)
33+ updateURL(p.Data().ProfileURL, hostname)
34+ updateURL(p.Data().ValidateURL, hostname)
35+ }
36+ return p
37+}
38+
39+func testKeycloakBackend(payload string) *httptest.Server {
40+ path := "/api/v3/user"
41+
42+ return httptest.NewServer(http.HandlerFunc(
43+ func(w http.ResponseWriter, r *http.Request) {
44+ url := r.URL
45+ if url.Path != path {
46+ w.WriteHeader(404)
47+ } else if r.Header.Get("Authorization") != bearerAccessToken {
48+ w.WriteHeader(403)
49+ } else {
50+ w.WriteHeader(200)
51+ w.Write([]byte(payload))
52+ }
53+ }))
54+}
55+
56+func TestKeycloakProviderDefaults(t *testing.T) {
57+ p := testKeycloakProvider("", "")
58+ assert.NotEqual(t, nil, p)
59+ assert.Equal(t, "Keycloak", p.Data().ProviderName)
60+ assert.Equal(t, "https://keycloak.org/oauth/authorize",
61+ p.Data().LoginURL.String())
62+ assert.Equal(t, "https://keycloak.org/oauth/token",
63+ p.Data().RedeemURL.String())
64+ assert.Equal(t, "https://keycloak.org/api/v3/user",
65+ p.Data().ValidateURL.String())
66+ assert.Equal(t, "api", p.Data().Scope)
67+}
68+
69+func TestKeycloakProviderOverrides(t *testing.T) {
70+ p := NewKeycloakProvider(
71+ &ProviderData{
72+ LoginURL: &url.URL{
73+ Scheme: "https",
74+ Host: "example.com",
75+ Path: "/oauth/auth"},
76+ RedeemURL: &url.URL{
77+ Scheme: "https",
78+ Host: "example.com",
79+ Path: "/oauth/token"},
80+ ValidateURL: &url.URL{
81+ Scheme: "https",
82+ Host: "example.com",
83+ Path: "/api/v3/user"},
84+ Scope: "profile"})
85+ assert.NotEqual(t, nil, p)
86+ assert.Equal(t, "Keycloak", p.Data().ProviderName)
87+ assert.Equal(t, "https://example.com/oauth/auth",
88+ p.Data().LoginURL.String())
89+ assert.Equal(t, "https://example.com/oauth/token",
90+ p.Data().RedeemURL.String())
91+ assert.Equal(t, "https://example.com/api/v3/user",
92+ p.Data().ValidateURL.String())
93+ assert.Equal(t, "profile", p.Data().Scope)
94+}
95+
96+func TestKeycloakProviderGetEmailAddress(t *testing.T) {
97+ b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\"}")
98+ defer b.Close()
99+
100+ bURL, _ := url.Parse(b.URL)
101+ p := testKeycloakProvider(bURL.Host, "")
102+
103+ session := &sessions.SessionState{AccessToken: imaginaryAccessToken}
104+ email, err := p.GetEmailAddress(session)
105+ assert.Equal(t, nil, err)
106+ assert.Equal(t, "michael.bland@gsa.gov", email)
107+}
108+
109+func TestKeycloakProviderGetEmailAddressAndGroup(t *testing.T) {
110+ b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\", \"groups\": [\"test-grp1\", \"test-grp2\"]}")
111+ defer b.Close()
112+
113+ bURL, _ := url.Parse(b.URL)
114+ p := testKeycloakProvider(bURL.Host, "test-grp1")
115+
116+ session := &sessions.SessionState{AccessToken: imaginaryAccessToken}
117+ email, err := p.GetEmailAddress(session)
118+ assert.Equal(t, nil, err)
119+ assert.Equal(t, "michael.bland@gsa.gov", email)
120+}
121+
122+// Note that trying to trigger the "failed building request" case is not
123+// practical, since the only way it can fail is if the URL fails to parse.
124+func TestKeycloakProviderGetEmailAddressFailedRequest(t *testing.T) {
125+ b := testKeycloakBackend("unused payload")
126+ defer b.Close()
127+
128+ bURL, _ := url.Parse(b.URL)
129+ p := testKeycloakProvider(bURL.Host, "")
130+
131+ // We'll trigger a request failure by using an unexpected access
132+ // token. Alternatively, we could allow the parsing of the payload as
133+ // JSON to fail.
134+ session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
135+ email, err := p.GetEmailAddress(session)
136+ assert.NotEqual(t, nil, err)
137+ assert.Equal(t, "", email)
138+}
139+
140+func TestKeycloakProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
141+ b := testKeycloakBackend("{\"foo\": \"bar\"}")
142+ defer b.Close()
143+
144+ bURL, _ := url.Parse(b.URL)
145+ p := testKeycloakProvider(bURL.Host, "")
146+
147+ session := &sessions.SessionState{AccessToken: imaginaryAccessToken}
148+ email, err := p.GetEmailAddress(session)
149+ assert.NotEqual(t, nil, err)
150+ assert.Equal(t, "", email)
151+}
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":