git.iko.soy/typeable/oauth2-proxy
on master help sign in
Migrate Keycloak to EnrichSession & support multiple groups
commit3369799853efcdd8a7fdb8e9cf3e8a7a2ba49f3b authorNick Meves <nick.meves@greenhouse.io> dateSat 12 Dec 2020, 12:57 (6 years ago) parent89e0a77
4 files changed+182−101
pkg/apis/options/options.go+2−2 pkg/validation/options.go+4−1 providers/keycloak.go+17−25 providers/keycloak_test.go+159−73
pkg/apis/options/options.go+2−2
@@ -36,7 +36,7 @@ type Options struct {
3636 TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file"`
3737
3838 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
39− KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group"`
39+ KeycloakGroups []string `flag:"keycloak-group" cfg:"keycloak_groups"`
4040 AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
4141 BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team"`
4242 BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository"`
@@ -181,7 +181,7 @@ func NewFlagSet() *pflag.FlagSet {
181181
182182 flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
183183 flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
184− flagSet.String("keycloak-group", "", "restrict login to members of this group.")
184+ flagSet.StringSlice("keycloak-group", []string{}, "restrict logins to members of these groups (may be given multiple times)")
185185 flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
186186 flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
187187 flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")
pkg/validation/options.go+4−1
@@ -263,7 +263,10 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
263263 p.SetRepo(o.GitHubRepo, o.GitHubToken)
264264 p.SetUsers(o.GitHubUsers)
265265 case *providers.KeycloakProvider:
266− p.SetGroup(o.KeycloakGroup)
266+ // Backwards compatibility with `--keycloak-group` option
267+ if len(o.KeycloakGroups) > 0 {
268+ p.SetAllowedGroups(o.KeycloakGroups)
269+ }
267270 case *providers.GoogleProvider:
268271 if o.GoogleServiceAccountJSON != "" {
269272 file, err := os.Open(o.GoogleServiceAccountJSON)
providers/keycloak.go+17−25
@@ -2,6 +2,7 @@ package providers
22
33import (
44 "context"
5+ "fmt"
56 "net/url"
67
78 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
@@ -11,7 +12,6 @@ import (
1112
1213type KeycloakProvider struct {
1314 *ProviderData
14− Group string
1515}
1616
1717var _ Provider = (*KeycloakProvider)(nil)
@@ -59,41 +59,33 @@ func NewKeycloakProvider(p *ProviderData) *KeycloakProvider {
5959 return &KeycloakProvider{ProviderData: p}
6060}
6161
62−func (p *KeycloakProvider) SetGroup(group string) {
63− p.Group = group
64−}
65−
66−func (p *KeycloakProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
62+func (p *KeycloakProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
6763 json, err := requests.New(p.ValidateURL.String()).
6864 WithContext(ctx).
6965 SetHeader("Authorization", "Bearer "+s.AccessToken).
7066 Do().
7167 UnmarshalJSON()
7268 if err != nil {
73− logger.Errorf("failed making request %s", err)
74− return "", err
69+ logger.Errorf("failed making request %v", err)
70+ return err
7571 }
7672
77− if p.Group != "" {
78− var groups, err = json.Get("groups").Array()
79− if err != nil {
80− logger.Printf("groups not found %s", err)
81− return "", err
82− }
83−
84− var found = false
85− for i := range groups {
86− if groups[i].(string) == p.Group {
87− found = true
88− break
73+ groups, err := json.Get("groups").StringArray()
74+ if err != nil {
75+ logger.Errorf("Warning: unable to extract groups from userinfo endpoint: %v", err)
76+ } else {
77+ for _, group := range groups {
78+ if group != "" {
79+ s.Groups = append(s.Groups, group)
8980 }
9081 }
82+ }
9183
92− if !found {
93− logger.Printf("group not found, access denied")
94− return "", nil
95− }
84+ email, err := json.Get("email").String()
85+ if err != nil {
86+ return fmt.Errorf("unable to extract email from userinfo endpoint: %v", err)
9687 }
88+ s.Email = email
9789
98− return json.Get("email").String()
90+ return nil
9991}
providers/keycloak_test.go+159−73
@@ -2,17 +2,33 @@ package providers
22
33import (
44 "context"
5+ "errors"
6+ "fmt"
57 "net/http"
68 "net/http/httptest"
79 "net/url"
810 "testing"
911
1012 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
13+ . "github.com/onsi/ginkgo"
14+ . "github.com/onsi/ginkgo/extensions/table"
1115 . "github.com/onsi/gomega"
1216 "github.com/stretchr/testify/assert"
1317)
1418
15−func testKeycloakProvider(hostname, group string) *KeycloakProvider {
19+const (
20+ keycloakAccessToken = "eyJKeycloak.eyJAccess.Token"
21+ keycloakUserinfoPath = "/api/v3/user"
22+
23+ // Userinfo Test Cases
24+ tcUIStandard = "userinfo-standard"
25+ tcUIFail = "userinfo-fail"
26+ tcUISingleGroup = "userinfo-single-group"
27+ tcUIMissingEmail = "userinfo-missing-email"
28+ tcUIMissingGroups = "userinfo-missing-groups"
29+)
30+
31+func testKeycloakProvider(backend *httptest.Server) (*KeycloakProvider, error) {
1632 p := NewKeycloakProvider(
1733 &ProviderData{
1834 ProviderName: "",
@@ -22,38 +38,165 @@ func testKeycloakProvider(hostname, group string) *KeycloakProvider {
2238 ValidateURL: &url.URL{},
2339 Scope: ""})
2440
25− if group != "" {
26− p.SetGroup(group)
27− }
41+ if backend != nil {
42+ bURL, err := url.Parse(backend.URL)
43+ if err != nil {
44+ return nil, err
45+ }
46+ hostname := bURL.Host
2847
29− if hostname != "" {
3048 updateURL(p.Data().LoginURL, hostname)
3149 updateURL(p.Data().RedeemURL, hostname)
3250 updateURL(p.Data().ProfileURL, hostname)
3351 updateURL(p.Data().ValidateURL, hostname)
3452 }
35− return p
36−}
3753
38−func testKeycloakBackend(payload string) *httptest.Server {
39− path := "/api/v3/user"
54+ return p, nil
55+}
4056
57+func testKeycloakBackend() *httptest.Server {
4158 return httptest.NewServer(http.HandlerFunc(
4259 func(w http.ResponseWriter, r *http.Request) {
43− url := r.URL
44− if url.Path != path {
60+ if r.URL.Path != keycloakUserinfoPath {
4561 w.WriteHeader(404)
46− } else if !IsAuthorizedInHeader(r.Header) {
47− w.WriteHeader(403)
48− } else {
62+ }
63+
64+ var err error
65+ switch r.URL.Query().Get("testcase") {
66+ case tcUIStandard:
67+ w.WriteHeader(200)
68+ _, err = w.Write([]byte(`
69+ {
70+ "email": "michael.bland@gsa.gov",
71+ "groups": [
72+ "test-grp1",
73+ "test-grp2"
74+ ]
75+ }
76+ `))
77+ case tcUIFail:
78+ w.WriteHeader(500)
79+ case tcUISingleGroup:
80+ w.WriteHeader(200)
81+ _, err = w.Write([]byte(`
82+ {
83+ "email": "michael.bland@gsa.gov",
84+ "groups": ["test-grp1"]
85+ }
86+ `))
87+ case tcUIMissingEmail:
88+ w.WriteHeader(200)
89+ _, err = w.Write([]byte(`
90+ {
91+ "groups": [
92+ "test-grp1",
93+ "test-grp2"
94+ ]
95+ }
96+ `))
97+ case tcUIMissingGroups:
4998 w.WriteHeader(200)
50− w.Write([]byte(payload))
99+ _, err = w.Write([]byte(`
100+ {
101+ "email": "michael.bland@gsa.gov"
102+ }
103+ `))
104+ default:
105+ w.WriteHeader(404)
106+ }
107+ if err != nil {
108+ panic(err)
51109 }
52110 }))
53111}
54112
113+var _ = Describe("Keycloak Provider Tests", func() {
114+ var p *KeycloakProvider
115+ var b *httptest.Server
116+
117+ BeforeEach(func() {
118+ b = testKeycloakBackend()
119+
120+ var err error
121+ p, err = testKeycloakProvider(b)
122+ Expect(err).To(BeNil())
123+ })
124+
125+ AfterEach(func() {
126+ b.Close()
127+ })
128+
129+ Context("EnrichSession", func() {
130+ type enrichSessionTableInput struct {
131+ testcase string
132+ expectedError error
133+ expectedEmail string
134+ expectedGroups []string
135+ }
136+
137+ DescribeTable("should return expected results",
138+ func(in enrichSessionTableInput) {
139+ var err error
140+ p.ValidateURL, err = url.Parse(
141+ fmt.Sprintf("%s%s?testcase=%s", b.URL, keycloakUserinfoPath, in.testcase),
142+ )
143+ Expect(err).To(BeNil())
144+
145+ session := &sessions.SessionState{AccessToken: keycloakAccessToken}
146+ err = p.EnrichSession(context.Background(), session)
147+
148+ if in.expectedError != nil {
149+ Expect(err).To(Equal(in.expectedError))
150+ } else {
151+ Expect(err).To(BeNil())
152+ }
153+
154+ Expect(session.Email).To(Equal(in.expectedEmail))
155+
156+ if in.expectedGroups != nil {
157+ Expect(session.Groups).To(Equal(in.expectedGroups))
158+ } else {
159+ Expect(session.Groups).To(BeNil())
160+ }
161+ },
162+ Entry("email and multiple groups", enrichSessionTableInput{
163+ testcase: tcUIStandard,
164+ expectedError: nil,
165+ expectedEmail: "michael.bland@gsa.gov",
166+ expectedGroups: []string{"test-grp1", "test-grp2"},
167+ }),
168+ Entry("email and single group", enrichSessionTableInput{
169+ testcase: tcUISingleGroup,
170+ expectedError: nil,
171+ expectedEmail: "michael.bland@gsa.gov",
172+ expectedGroups: []string{"test-grp1"},
173+ }),
174+ Entry("email and no groups", enrichSessionTableInput{
175+ testcase: tcUIMissingGroups,
176+ expectedError: nil,
177+ expectedEmail: "michael.bland@gsa.gov",
178+ expectedGroups: nil,
179+ }),
180+ Entry("missing email", enrichSessionTableInput{
181+ testcase: tcUIMissingEmail,
182+ expectedError: errors.New(
183+ "unable to extract email from userinfo endpoint: type assertion to string failed"),
184+ expectedEmail: "",
185+ expectedGroups: []string{"test-grp1", "test-grp2"},
186+ }),
187+ Entry("request failure", enrichSessionTableInput{
188+ testcase: tcUIFail,
189+ expectedError: errors.New(`unexpected status "500": `),
190+ expectedEmail: "",
191+ expectedGroups: nil,
192+ }),
193+ )
194+ })
195+})
196+
55197func TestKeycloakProviderDefaults(t *testing.T) {
56− p := testKeycloakProvider("", "")
198+ p, err := testKeycloakProvider(nil)
199+ assert.NoError(t, err)
57200 assert.NotEqual(t, nil, p)
58201 assert.Equal(t, "Keycloak", p.Data().ProviderName)
59202 assert.Equal(t, "https://keycloak.org/oauth/authorize",
@@ -104,60 +247,3 @@ func TestKeycloakProviderOverrides(t *testing.T) {
104247 p.Data().ValidateURL.String())
105248 assert.Equal(t, "profile", p.Data().Scope)
106249}
107−
108−func TestKeycloakProviderGetEmailAddress(t *testing.T) {
109− b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\"}")
110− defer b.Close()
111−
112− bURL, _ := url.Parse(b.URL)
113− p := testKeycloakProvider(bURL.Host, "")
114−
115− session := CreateAuthorizedSession()
116− email, err := p.GetEmailAddress(context.Background(), session)
117− assert.Equal(t, nil, err)
118− assert.Equal(t, "michael.bland@gsa.gov", email)
119−}
120−
121−func TestKeycloakProviderGetEmailAddressAndGroup(t *testing.T) {
122− b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\", \"groups\": [\"test-grp1\", \"test-grp2\"]}")
123− defer b.Close()
124−
125− bURL, _ := url.Parse(b.URL)
126− p := testKeycloakProvider(bURL.Host, "test-grp1")
127−
128− session := CreateAuthorizedSession()
129− email, err := p.GetEmailAddress(context.Background(), session)
130− assert.Equal(t, nil, err)
131− assert.Equal(t, "michael.bland@gsa.gov", email)
132−}
133−
134−// Note that trying to trigger the "failed building request" case is not
135−// practical, since the only way it can fail is if the URL fails to parse.
136−func TestKeycloakProviderGetEmailAddressFailedRequest(t *testing.T) {
137− b := testKeycloakBackend("unused payload")
138− defer b.Close()
139−
140− bURL, _ := url.Parse(b.URL)
141− p := testKeycloakProvider(bURL.Host, "")
142−
143− // We'll trigger a request failure by using an unexpected access
144− // token. Alternatively, we could allow the parsing of the payload as
145− // JSON to fail.
146− session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
147− email, err := p.GetEmailAddress(context.Background(), session)
148− assert.NotEqual(t, nil, err)
149− assert.Equal(t, "", email)
150−}
151−
152−func TestKeycloakProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
153− b := testKeycloakBackend("{\"foo\": \"bar\"}")
154− defer b.Close()
155−
156− bURL, _ := url.Parse(b.URL)
157− p := testKeycloakProvider(bURL.Host, "")
158−
159− session := CreateAuthorizedSession()
160− email, err := p.GetEmailAddress(context.Background(), session)
161− assert.NotEqual(t, nil, err)
162− assert.Equal(t, "", email)
163−}