@@ -2,17 +2,24 @@ package providers
22
33import (
44 "context"
5+ "errors"
6+ "fmt"
57 "net/http"
68 "net/http/httptest"
79 "net/url"
8− "testing"
910
1011 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12+ . "github.com/onsi/ginkgo"
13+ . "github.com/onsi/ginkgo/extensions/table"
1114 . "github.com/onsi/gomega"
12− "github.com/stretchr/testify/assert"
1315)
1416
15−func testKeycloakProvider(hostname, group string) *KeycloakProvider {
17+const (
18+ keycloakAccessToken = "eyJKeycloak.eyJAccess.Token"
19+ keycloakUserinfoPath = "/api/v3/user"
20+)
21+
22+func testKeycloakProvider(backend *httptest.Server) (*KeycloakProvider, error) {
1623 p := NewKeycloakProvider(
1724 &ProviderData{
1825 ProviderName: "",
@@ -22,142 +29,182 @@ func testKeycloakProvider(hostname, group string) *KeycloakProvider {
2229 ValidateURL: &url.URL{},
2330 Scope: ""})
2431
25− if group != "" {
26− p.SetGroup(group)
27− }
32+ if backend != nil {
33+ bURL, err := url.Parse(backend.URL)
34+ if err != nil {
35+ return nil, err
36+ }
37+ hostname := bURL.Host
2838
29− if hostname != "" {
3039 updateURL(p.Data().LoginURL, hostname)
3140 updateURL(p.Data().RedeemURL, hostname)
3241 updateURL(p.Data().ProfileURL, hostname)
3342 updateURL(p.Data().ValidateURL, hostname)
3443 }
35− return p
36−}
3744
38−func testKeycloakBackend(payload string) *httptest.Server {
39− path := "/api/v3/user"
40−
41− return httptest.NewServer(http.HandlerFunc(
42− func(w http.ResponseWriter, r *http.Request) {
43− url := r.URL
44− if url.Path != path {
45− w.WriteHeader(404)
46− } else if !IsAuthorizedInHeader(r.Header) {
47− w.WriteHeader(403)
48− } else {
49− w.WriteHeader(200)
50− w.Write([]byte(payload))
51− }
52− }))
45+ return p, nil
5346}
5447
55−func TestKeycloakProviderDefaults(t *testing.T) {
56− p := testKeycloakProvider("", "")
57− assert.NotEqual(t, nil, p)
58− assert.Equal(t, "Keycloak", p.Data().ProviderName)
59− assert.Equal(t, "https://keycloak.org/oauth/authorize",
60− p.Data().LoginURL.String())
61− assert.Equal(t, "https://keycloak.org/oauth/token",
62− p.Data().RedeemURL.String())
63− assert.Equal(t, "https://keycloak.org/api/v3/user",
64− p.Data().ValidateURL.String())
65− assert.Equal(t, "api", p.Data().Scope)
66−}
67−
68−func TestNewKeycloakProvider(t *testing.T) {
69− g := NewWithT(t)
70−
71− // Test that defaults are set when calling for a new provider with nothing set
72− providerData := NewKeycloakProvider(&ProviderData{}).Data()
73− g.Expect(providerData.ProviderName).To(Equal("Keycloak"))
74− g.Expect(providerData.LoginURL.String()).To(Equal("https://keycloak.org/oauth/authorize"))
75− g.Expect(providerData.RedeemURL.String()).To(Equal("https://keycloak.org/oauth/token"))
76− g.Expect(providerData.ProfileURL.String()).To(Equal(""))
77− g.Expect(providerData.ValidateURL.String()).To(Equal("https://keycloak.org/api/v3/user"))
78− g.Expect(providerData.Scope).To(Equal("api"))
79−}
80−
81−func TestKeycloakProviderOverrides(t *testing.T) {
82− p := NewKeycloakProvider(
83− &ProviderData{
84− LoginURL: &url.URL{
85− Scheme: "https",
86− Host: "example.com",
87− Path: "/oauth/auth"},
88− RedeemURL: &url.URL{
89− Scheme: "https",
90− Host: "example.com",
91− Path: "/oauth/token"},
92− ValidateURL: &url.URL{
93− Scheme: "https",
94− Host: "example.com",
95− Path: "/api/v3/user"},
96− Scope: "profile"})
97− assert.NotEqual(t, nil, p)
98− assert.Equal(t, "Keycloak", p.Data().ProviderName)
99− assert.Equal(t, "https://example.com/oauth/auth",
100− p.Data().LoginURL.String())
101− assert.Equal(t, "https://example.com/oauth/token",
102− p.Data().RedeemURL.String())
103− assert.Equal(t, "https://example.com/api/v3/user",
104− p.Data().ValidateURL.String())
105− assert.Equal(t, "profile", p.Data().Scope)
106−}
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−}
48+var _ = Describe("Keycloak Provider Tests", func() {
49+ Context("New Provider Init", func() {
50+ It("uses defaults", func() {
51+ providerData := NewKeycloakProvider(&ProviderData{}).Data()
52+ Expect(providerData.ProviderName).To(Equal("Keycloak"))
53+ Expect(providerData.LoginURL.String()).To(Equal("https://keycloak.org/oauth/authorize"))
54+ Expect(providerData.RedeemURL.String()).To(Equal("https://keycloak.org/oauth/token"))
55+ Expect(providerData.ProfileURL.String()).To(Equal(""))
56+ Expect(providerData.ValidateURL.String()).To(Equal("https://keycloak.org/api/v3/user"))
57+ Expect(providerData.Scope).To(Equal("api"))
58+ })
59+
60+ It("overrides defaults", func() {
61+ p := NewKeycloakProvider(
62+ &ProviderData{
63+ LoginURL: &url.URL{
64+ Scheme: "https",
65+ Host: "example.com",
66+ Path: "/oauth/auth"},
67+ RedeemURL: &url.URL{
68+ Scheme: "https",
69+ Host: "example.com",
70+ Path: "/oauth/token"},
71+ ProfileURL: &url.URL{
72+ Scheme: "https",
73+ Host: "example.com",
74+ Path: "/api/v3/user"},
75+ ValidateURL: &url.URL{
76+ Scheme: "https",
77+ Host: "example.com",
78+ Path: "/api/v3/user"},
79+ Scope: "profile"})
80+ providerData := p.Data()
81+
82+ Expect(providerData.ProviderName).To(Equal("Keycloak"))
83+ Expect(providerData.LoginURL.String()).To(Equal("https://example.com/oauth/auth"))
84+ Expect(providerData.RedeemURL.String()).To(Equal("https://example.com/oauth/token"))
85+ Expect(providerData.ProfileURL.String()).To(Equal("https://example.com/api/v3/user"))
86+ Expect(providerData.ValidateURL.String()).To(Equal("https://example.com/api/v3/user"))
87+ Expect(providerData.Scope).To(Equal("profile"))
88+ })
89+ })
90+
91+ Context("EnrichSession", func() {
92+ type enrichSessionTableInput struct {
93+ backendHandler http.HandlerFunc
94+ expectedError error
95+ expectedEmail string
96+ expectedGroups []string
97+ }
98+
99+ DescribeTable("should return expected results",
100+ func(in enrichSessionTableInput) {
101+ backend := httptest.NewServer(in.backendHandler)
102+ p, err := testKeycloakProvider(backend)
103+ Expect(err).To(BeNil())
104+
105+ p.ProfileURL, err = url.Parse(
106+ fmt.Sprintf("%s%s", backend.URL, keycloakUserinfoPath),
107+ )
108+ Expect(err).To(BeNil())
109+
110+ session := &sessions.SessionState{AccessToken: keycloakAccessToken}
111+ err = p.EnrichSession(context.Background(), session)
112+
113+ if in.expectedError != nil {
114+ Expect(err).To(Equal(in.expectedError))
115+ } else {
116+ Expect(err).To(BeNil())
117+ }
118+
119+ Expect(session.Email).To(Equal(in.expectedEmail))
120+
121+ if in.expectedGroups != nil {
122+ Expect(session.Groups).To(Equal(in.expectedGroups))
123+ } else {
124+ Expect(session.Groups).To(BeNil())
125+ }
126+ },
127+ Entry("email and multiple groups", enrichSessionTableInput{
128+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
129+ w.WriteHeader(200)
130+ _, err := w.Write([]byte(`
131+ {
132+ "email": "michael.bland@gsa.gov",
133+ "groups": [
134+ "test-grp1",
135+ "test-grp2"
136+ ]
137+ }
138+ `))
139+ if err != nil {
140+ panic(err)
141+ }
142+ },
143+ expectedError: nil,
144+ expectedEmail: "michael.bland@gsa.gov",
145+ expectedGroups: []string{"test-grp1", "test-grp2"},
146+ }),
147+ Entry("email and single group", enrichSessionTableInput{
148+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
149+ w.WriteHeader(200)
150+ _, err := w.Write([]byte(`
151+ {
152+ "email": "michael.bland@gsa.gov",
153+ "groups": ["test-grp1"]
154+ }
155+ `))
156+ if err != nil {
157+ panic(err)
158+ }
159+ },
160+ expectedError: nil,
161+ expectedEmail: "michael.bland@gsa.gov",
162+ expectedGroups: []string{"test-grp1"},
163+ }),
164+ Entry("email and no groups", enrichSessionTableInput{
165+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
166+ w.WriteHeader(200)
167+ _, err := w.Write([]byte(`
168+ {
169+ "email": "michael.bland@gsa.gov"
170+ }
171+ `))
172+ if err != nil {
173+ panic(err)
174+ }
175+ },
176+ expectedError: nil,
177+ expectedEmail: "michael.bland@gsa.gov",
178+ expectedGroups: nil,
179+ }),
180+ Entry("missing email", enrichSessionTableInput{
181+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
182+ w.WriteHeader(200)
183+ _, err := w.Write([]byte(`
184+ {
185+ "groups": [
186+ "test-grp1",
187+ "test-grp2"
188+ ]
189+ }
190+ `))
191+ if err != nil {
192+ panic(err)
193+ }
194+ },
195+ expectedError: errors.New(
196+ "unable to extract email from userinfo endpoint: type assertion to string failed"),
197+ expectedEmail: "",
198+ expectedGroups: []string{"test-grp1", "test-grp2"},
199+ }),
200+ Entry("request failure", enrichSessionTableInput{
201+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
202+ w.WriteHeader(500)
203+ },
204+ expectedError: errors.New(`unexpected status "500": `),
205+ expectedEmail: "",
206+ expectedGroups: nil,
207+ }),
208+ )
209+ })
210+})