@@ -0,0 +1,493 @@
1+package providers
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "errors"
7+ "fmt"
8+ "net/http"
9+ "net/http/httptest"
10+ "net/url"
11+ "testing"
12+
13+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
14+
15+ "github.com/coreos/go-oidc/v3/oidc"
16+ "github.com/stretchr/testify/assert"
17+
18+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
19+)
20+
21+func newCidaasProvider(serverURL *url.URL) *CIDAASProvider {
22+ providerData := &ProviderData{
23+ ProviderName: "cidaas",
24+ ClientID: oidcClientID,
25+ ClientSecret: oidcSecret,
26+ LoginURL: &url.URL{
27+ Scheme: serverURL.Scheme,
28+ Host: serverURL.Host,
29+ Path: "/login/oauth/authorize"},
30+ RedeemURL: &url.URL{
31+ Scheme: serverURL.Scheme,
32+ Host: serverURL.Host,
33+ Path: "/login/oauth/access_token"},
34+ ProfileURL: &url.URL{
35+ Scheme: serverURL.Scheme,
36+ Host: serverURL.Host,
37+ Path: "/profile"},
38+ ValidateURL: &url.URL{
39+ Scheme: serverURL.Scheme,
40+ Host: serverURL.Host,
41+ Path: "/api"},
42+ Scope: "openid profile offline_access roles groups",
43+ EmailClaim: "email",
44+ GroupsClaim: "groups",
45+ Verifier: oidc.NewVerifier(
46+ oidcIssuer,
47+ mockJWKS{},
48+ &oidc.Config{ClientID: oidcClientID},
49+ ),
50+ }
51+ cfg := options.Provider{
52+ Type: options.CidaasProvider,
53+ }
54+
55+ p := NewCIDAASProvider(providerData, cfg)
56+
57+ return p
58+}
59+
60+func newCidaasServer(pathBodyMap map[string][]byte) (*url.URL, *httptest.Server) {
61+ s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
62+ body, ok := pathBodyMap[r.URL.Path]
63+ if !ok {
64+ rw.WriteHeader(404)
65+ return
66+ }
67+ rw.Header().Add("content-type", "application/json")
68+ _, _ = rw.Write(body)
69+ }))
70+ u, _ := url.Parse(s.URL)
71+ return u, s
72+}
73+
74+func newTestCidaasSetup(pathToBodyMap map[string][]byte) (*httptest.Server, *CIDAASProvider) {
75+ redeemURL, server := newCidaasServer(pathToBodyMap)
76+ provider := newCidaasProvider(redeemURL)
77+ return server, provider
78+}
79+
80+func TestCidaasProvider_EnrichSession(t *testing.T) {
81+ testCases := map[string]struct {
82+ ExistingSession *sessions.SessionState
83+ EmailClaim string
84+ GroupsClaim string
85+ ProfileJSON map[string]interface{}
86+ ExpectedError error
87+ ExpectedSession *sessions.SessionState
88+ }{
89+ "Missing Email Only in Profile URL": {
90+ ExistingSession: &sessions.SessionState{
91+ User: "missing.email",
92+ IDToken: idToken,
93+ AccessToken: accessToken,
94+ RefreshToken: refreshToken,
95+ },
96+ EmailClaim: "email",
97+ GroupsClaim: "groups",
98+ ProfileJSON: map[string]interface{}{
99+ "email": "found@email.com",
100+ },
101+ ExpectedError: nil,
102+ ExpectedSession: &sessions.SessionState{
103+ User: "missing.email",
104+ Email: "found@email.com",
105+ IDToken: idToken,
106+ AccessToken: accessToken,
107+ RefreshToken: refreshToken,
108+ },
109+ },
110+ "Missing Email with Custom Claim": {
111+ ExistingSession: &sessions.SessionState{
112+ User: "missing.email",
113+ Groups: []string{"already", "populated"},
114+ IDToken: idToken,
115+ AccessToken: accessToken,
116+ RefreshToken: refreshToken,
117+ },
118+ EmailClaim: "weird",
119+ GroupsClaim: "groups",
120+ ProfileJSON: map[string]interface{}{
121+ "weird": "weird@claim.com",
122+ "groups": []map[string]interface{}{
123+ {
124+ "groupId": "CIDAAS_USERS",
125+ "roles": []string{"USER"},
126+ },
127+ },
128+ "roles": []string{"USER"},
129+ },
130+ ExpectedError: nil,
131+ ExpectedSession: &sessions.SessionState{
132+ User: "missing.email",
133+ Email: "weird@claim.com",
134+ Groups: []string{"CIDAAS_USERS:USER", "cidaas:USER"},
135+ IDToken: idToken,
136+ AccessToken: accessToken,
137+ RefreshToken: refreshToken,
138+ },
139+ },
140+ "Missing Email not in Profile URL": {
141+ ExistingSession: &sessions.SessionState{
142+ User: "missing.email",
143+ Groups: []string{"already", "populated"},
144+ IDToken: idToken,
145+ AccessToken: accessToken,
146+ RefreshToken: refreshToken,
147+ },
148+ EmailClaim: "email",
149+ GroupsClaim: "groups",
150+ ProfileJSON: map[string]interface{}{
151+ "groups": []map[string]interface{}{
152+ {
153+ "groupId": "CIDAAS_USERS",
154+ "roles": []string{"USER"},
155+ },
156+ },
157+ "roles": []string{"USER"},
158+ },
159+ ExpectedError: errors.New("neither the id_token nor the profileURL set an email"),
160+ ExpectedSession: &sessions.SessionState{
161+ User: "missing.email",
162+ Groups: []string{"CIDAAS_USERS:USER", "cidaas:USER"},
163+ IDToken: idToken,
164+ AccessToken: accessToken,
165+ RefreshToken: refreshToken,
166+ },
167+ },
168+ "Missing Groups": {
169+ ExistingSession: &sessions.SessionState{
170+ User: "already",
171+ Email: "already@populated.com",
172+ Groups: nil,
173+ IDToken: idToken,
174+ AccessToken: accessToken,
175+ RefreshToken: refreshToken,
176+ },
177+ EmailClaim: "email",
178+ GroupsClaim: "groups",
179+ ProfileJSON: map[string]interface{}{
180+ "email": "new@thing.com",
181+ "groups": []map[string]interface{}{
182+ {
183+ "groupId": "CIDAAS_USERS",
184+ "roles": []string{"USER"},
185+ },
186+ },
187+ "roles": []string{"USER"},
188+ },
189+ ExpectedError: nil,
190+ ExpectedSession: &sessions.SessionState{
191+ User: "already",
192+ Email: "already@populated.com",
193+ Groups: []string{"CIDAAS_USERS:USER", "cidaas:USER"},
194+ IDToken: idToken,
195+ AccessToken: accessToken,
196+ RefreshToken: refreshToken,
197+ },
198+ },
199+ "Empty Groups Claims": {
200+ ExistingSession: &sessions.SessionState{
201+ User: "already",
202+ Email: "already@populated.com",
203+ Groups: []string{},
204+ IDToken: idToken,
205+ AccessToken: accessToken,
206+ RefreshToken: refreshToken,
207+ },
208+ EmailClaim: "email",
209+ GroupsClaim: "groups",
210+ ProfileJSON: map[string]interface{}{
211+ "email": "new@thing.com",
212+ "groups": []map[string]interface{}{
213+ {
214+ "groupId": "CIDAAS_USERS",
215+ "roles": []string{"USER"},
216+ },
217+ },
218+ "roles": []string{"USER"},
219+ },
220+ ExpectedError: nil,
221+ ExpectedSession: &sessions.SessionState{
222+ User: "already",
223+ Email: "already@populated.com",
224+ Groups: []string{"CIDAAS_USERS:USER", "cidaas:USER"},
225+ IDToken: idToken,
226+ AccessToken: accessToken,
227+ RefreshToken: refreshToken,
228+ },
229+ },
230+ "Missing Groups with Custom Claim": {
231+ ExistingSession: &sessions.SessionState{
232+ User: "already",
233+ Email: "already@populated.com",
234+ Groups: nil,
235+ IDToken: idToken,
236+ AccessToken: accessToken,
237+ RefreshToken: refreshToken,
238+ },
239+ EmailClaim: "email",
240+ GroupsClaim: "groups2",
241+ ProfileJSON: map[string]interface{}{
242+ "email": "already@populated.com",
243+ "groups2": []map[string]interface{}{
244+ {
245+ "sub": "aa5181ea-0841-4ea7-b67f-81882f153d40",
246+ "groupId": "CIDAAS_ADMINS",
247+ "path": "/CIDAAS_ADMINS/",
248+ "roles": []string{"ADMIN"},
249+ },
250+ {
251+ "sub": "aa5181ea-0841-4ea7-b67f-81882f153d39",
252+ "groupId": "customers",
253+ "groupType": "Customers",
254+ "path": "/customers/",
255+ "roles": []string{
256+ "CUSTOMER_ACCOUNT_LOGIN",
257+ "GROUP_ADMIN",
258+ },
259+ },
260+ {
261+ "groupId": "CIDAAS_USERS",
262+ "roles": []string{"USER"},
263+ },
264+ },
265+ "roles": []string{"USER"},
266+ },
267+ ExpectedError: nil,
268+ ExpectedSession: &sessions.SessionState{
269+ User: "already",
270+ Email: "already@populated.com",
271+ Groups: []string{"CIDAAS_ADMINS:ADMIN", "customers:CUSTOMER_ACCOUNT_LOGIN", "customers:GROUP_ADMIN", "CIDAAS_USERS:USER", "cidaas:USER"},
272+ IDToken: idToken,
273+ AccessToken: accessToken,
274+ RefreshToken: refreshToken,
275+ },
276+ },
277+ "Just format Groups": {
278+ ExistingSession: &sessions.SessionState{
279+ User: "already",
280+ Email: "already@populated.com",
281+ Groups: nil,
282+ IDToken: idToken,
283+ AccessToken: accessToken,
284+ RefreshToken: refreshToken,
285+ },
286+ EmailClaim: "email",
287+ GroupsClaim: "groups2",
288+ ProfileJSON: map[string]interface{}{
289+ "email": "already@populated.com",
290+ "groups2": []map[string]interface{}{
291+ {
292+ "sub": "aa5181ea-0841-4ea7-b67f-81882f153d39",
293+ "groupId": "customers",
294+ "groupType": "Customers",
295+ "path": "/customers/",
296+ "roles": []string{
297+ "CUSTOMER_ACCOUNT_LOGIN",
298+ "GROUP_ADMIN",
299+ },
300+ },
301+ {
302+ "groupId": "CIDAAS_USERS",
303+ "roles": []string{"USER"},
304+ },
305+ },
306+ "roles": []string{"USER"},
307+ },
308+ ExpectedError: nil,
309+ ExpectedSession: &sessions.SessionState{
310+ User: "already",
311+ Email: "already@populated.com",
312+ Groups: []string{"customers:CUSTOMER_ACCOUNT_LOGIN", "customers:GROUP_ADMIN", "CIDAAS_USERS:USER", "cidaas:USER"},
313+ IDToken: idToken,
314+ AccessToken: accessToken,
315+ RefreshToken: refreshToken,
316+ },
317+ },
318+ "Missing Groups String Profile URL Response": {
319+ ExistingSession: &sessions.SessionState{
320+ User: "already",
321+ Email: "already@populated.com",
322+ Groups: nil,
323+ IDToken: idToken,
324+ AccessToken: accessToken,
325+ RefreshToken: refreshToken,
326+ },
327+ EmailClaim: "email",
328+ GroupsClaim: "groups",
329+ ProfileJSON: map[string]interface{}{
330+ "groups": []map[string]interface{}{
331+ {
332+ "sub": "aa5181ea-0841-4ea7-b67f-81882f153d40",
333+ "groupId": "CIDAAS_ADMINS",
334+ "path": "/CIDAAS_ADMINS/",
335+ "roles": []string{"ADMIN"},
336+ },
337+ {
338+ "sub": "aa5181ea-0841-4ea7-b67f-81882f153d39",
339+ "groupId": "customers",
340+ "groupType": "Customers",
341+ "path": "/customers/",
342+ "roles": []string{
343+ "CUSTOMER_ACCOUNT_LOGIN",
344+ "GROUP_ADMIN",
345+ },
346+ },
347+ {
348+ "groupId": "CIDAAS_USERS",
349+ "roles": []string{"USER"},
350+ },
351+ },
352+ "roles": []string{"USER"},
353+ },
354+ ExpectedError: nil,
355+ ExpectedSession: &sessions.SessionState{
356+ User: "already",
357+ Email: "already@populated.com",
358+ Groups: []string{"CIDAAS_ADMINS:ADMIN", "customers:CUSTOMER_ACCOUNT_LOGIN", "customers:GROUP_ADMIN", "CIDAAS_USERS:USER", "cidaas:USER"},
359+ IDToken: idToken,
360+ AccessToken: accessToken,
361+ RefreshToken: refreshToken,
362+ },
363+ },
364+ "Missing Groups in both Claims and Profile URL": {
365+ ExistingSession: &sessions.SessionState{
366+ User: "already",
367+ Email: "already@populated.com",
368+ IDToken: idToken,
369+ AccessToken: accessToken,
370+ RefreshToken: refreshToken,
371+ },
372+ EmailClaim: "email",
373+ GroupsClaim: "groups",
374+ ProfileJSON: map[string]interface{}{
375+ "email": "new@thing.com",
376+ },
377+ ExpectedError: nil,
378+ ExpectedSession: &sessions.SessionState{
379+ User: "already",
380+ Email: "already@populated.com",
381+ IDToken: idToken,
382+ AccessToken: accessToken,
383+ RefreshToken: refreshToken,
384+ },
385+ },
386+ }
387+ for testName, tc := range testCases {
388+ t.Run(testName, func(t *testing.T) {
389+ path := "/userinfo/"
390+ jsonResp, err := json.Marshal(tc.ProfileJSON)
391+ assert.NoError(t, err)
392+
393+ server, provider := newTestCidaasSetup(map[string][]byte{path: jsonResp})
394+ provider.ProfileURL, err = url.Parse(fmt.Sprintf("%s%s", server.URL, path))
395+ assert.NoError(t, err)
396+
397+ provider.EmailClaim = tc.EmailClaim
398+ provider.GroupsClaim = tc.GroupsClaim
399+ defer server.Close()
400+
401+ err = provider.EnrichSession(context.Background(), tc.ExistingSession)
402+ if tc.ExpectedError != nil {
403+ assert.EqualError(t, err, tc.ExpectedError.Error())
404+ } else {
405+ assert.NoError(t, err)
406+ }
407+ assert.Equal(t, *tc.ExpectedSession, *tc.ExistingSession)
408+ })
409+ }
410+}
411+
412+func TestCidaasProvider_RefreshSession(t *testing.T) {
413+ testCases := map[string]struct {
414+ ExistingSession *sessions.SessionState
415+ EmailClaim string
416+ GroupsClaim string
417+ ProfileJSON map[string]interface{}
418+ RedeemJSON redeemTokenResponse
419+ ExpectedRefreshed bool
420+ ExpectedError error
421+ ExpectedEmail string
422+ ExpectedUser string
423+ }{
424+ "Refresh session successfully": {
425+ ExistingSession: &sessions.SessionState{
426+ User: "session.is.not.locked",
427+ Email: "found@email.com",
428+ IDToken: idToken,
429+ AccessToken: accessToken,
430+ RefreshToken: refreshToken,
431+ },
432+ RedeemJSON: redeemTokenResponse{
433+ AccessToken: accessToken,
434+ ExpiresIn: 10,
435+ TokenType: "Bearer",
436+ RefreshToken: refreshToken,
437+ },
438+ ExpectedRefreshed: true,
439+ ExpectedError: nil,
440+ ExpectedEmail: defaultIDToken.Email,
441+ ExpectedUser: defaultIDToken.Subject,
442+ },
443+ "Unable to refresh session": {
444+ ExistingSession: &sessions.SessionState{
445+ User: "session.is.unable.to.refresh",
446+ Email: "found@email.com",
447+ IDToken: idToken,
448+ AccessToken: accessToken,
449+ RefreshToken: refreshToken,
450+ },
451+ ExpectedRefreshed: false,
452+ ExpectedError: fmt.Errorf("unable to redeem refresh token: failed to get token: oauth2: server response missing access_token"),
453+ ExpectedUser: "session.is.unable.to.refresh",
454+ ExpectedEmail: "found@email.com",
455+ },
456+ }
457+ for testName, tc := range testCases {
458+ t.Run(testName, func(t *testing.T) {
459+ idToken, _ := newSignedTestIDToken(defaultIDToken)
460+ tc.RedeemJSON.IDToken = idToken
461+ redeemPath := "/token/"
462+ redeemJSONResp, err := json.Marshal(tc.RedeemJSON)
463+ assert.NoError(t, err)
464+
465+ serverURL, server := newCidaasServer(
466+ map[string][]byte{
467+ redeemPath: redeemJSONResp,
468+ })
469+ provider := newCidaasProvider(serverURL)
470+
471+ // Disable session enrichment, because we want to focus on refreshing logic
472+ provider.ProfileURL, err = url.Parse("")
473+ assert.NoError(t, err)
474+ provider.RedeemURL, err = url.Parse(fmt.Sprintf("%s%s", server.URL, redeemPath))
475+ assert.NoError(t, err)
476+
477+ provider.GroupsClaim = tc.GroupsClaim
478+ defer server.Close()
479+
480+ var refreshed bool
481+ refreshed, err = provider.RefreshSession(context.Background(), tc.ExistingSession)
482+
483+ if tc.ExpectedError != nil {
484+ assert.EqualError(t, err, tc.ExpectedError.Error())
485+ } else {
486+ assert.NoError(t, err)
487+ }
488+ assert.Equal(t, tc.ExpectedRefreshed, refreshed)
489+ assert.Equal(t, tc.ExpectedEmail, tc.ExistingSession.Email)
490+ assert.Equal(t, tc.ExpectedUser, tc.ExistingSession.User)
491+ })
492+ }
493+}