feat: add Cidaas provider (#2273)
* Add sensible logging flag to default setup for logger * Fix default value flag for sensitive logging * Remove sensitive logging changes * Add Cidaas provider * Update CHANGELOG.md * Add required groups scope to defaults * Fix tests * Remove if block with protected resource * Fix linting * Adjust provider sorting, fixes * Directly handle error return Co-authored-by: Jan Larwig <jan@larwig.com> * Use less deep nesting Co-authored-by: Jan Larwig <jan@larwig.com> * Directly handle returned error Co-authored-by: Jan Larwig <jan@larwig.com> * Pass provider options to Cidaas provider Co-authored-by: Jan Larwig <jan@larwig.com> * Add import for provider options * Fix tests * Fix linting * Add Cidaas doc page * Add Cidaas provider doc page to overview * Fix link in docs * Fix link in docs * Add link to Cidaas * fix provider order in docs and changelog position Signed-off-by: Jan Larwig <jan@larwig.com> --------- Signed-off-by: Jan Larwig <jan@larwig.com> Co-authored-by: Teko012 <112829523+Teko012@users.noreply.github.com> Co-authored-by: Jan Larwig <jan@larwig.com> Co-authored-by: Kevin Kreitner <kevinkreitner@gmail.com>
This commit is contained in:
		
							parent
							
								
									9667bce094
								
							
						
					
					
						commit
						4c86a4d574
					
				|  | @ -8,6 +8,8 @@ | ||||||
| 
 | 
 | ||||||
| ## Changes since v7.11.0 | ## Changes since v7.11.0 | ||||||
| 
 | 
 | ||||||
|  | - [#2273](https://github.com/oauth2-proxy/oauth2-proxy/pull/2273) feat: add Cidaas provider (@Bibob7, @Teko012) | ||||||
|  | 
 | ||||||
| # V7.11.0 | # V7.11.0 | ||||||
| 
 | 
 | ||||||
| ## Release Highlights | ## Release Highlights | ||||||
|  |  | ||||||
|  | @ -0,0 +1,37 @@ | ||||||
|  | --- | ||||||
|  | id: cidaas | ||||||
|  | title: Cidaas | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | [Cidaas](https://www.cidaas.com/) is an Identity as a Service (IDaaS) solution that provides authentication and authorization services.  | ||||||
|  | It supports various protocols including OpenID Connect, OAuth 2.0, and SAML. | ||||||
|  | 
 | ||||||
|  | However, Cidaas provides groups and their roles as hierarchical claims, which are not supported by oauth2-proxy yet. | ||||||
|  | The Cidaas provider transforms the hierarchical claims into a flat list of groups, which can be used by oauth2-proxy. | ||||||
|  | 
 | ||||||
|  | Example of groups and roles in Cidaas: | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "groups": [ | ||||||
|  |     { | ||||||
|  |       "groupId": "group1", | ||||||
|  |       "roles": ["role1", "role2"] | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "groupId": "group2", | ||||||
|  |       "roles": ["role3"] | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | This will be transformed into a flat list of groups: | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "groups": ["group1:role1", "group2:role2", "group2:role3"] | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Apart from that the Cidaas provider inherits all the features of the [OpenID Connect provider](openid_connect.md). | ||||||
|  | @ -10,6 +10,7 @@ Valid providers are : | ||||||
| 
 | 
 | ||||||
| - [ADFS](adfs.md) | - [ADFS](adfs.md) | ||||||
| - [Bitbucket](bitbucket.md) | - [Bitbucket](bitbucket.md) | ||||||
|  | - [Cidaas](cidaas.md) | ||||||
| - [DigitalOcean](digitalocean.md) | - [DigitalOcean](digitalocean.md) | ||||||
| - [Facebook](facebook.md) | - [Facebook](facebook.md) | ||||||
| - [Gitea](gitea.md) | - [Gitea](gitea.md) | ||||||
|  |  | ||||||
|  | @ -115,6 +115,9 @@ const ( | ||||||
| 	// BitbucketProvider is the provider type for Bitbucket
 | 	// BitbucketProvider is the provider type for Bitbucket
 | ||||||
| 	BitbucketProvider ProviderType = "bitbucket" | 	BitbucketProvider ProviderType = "bitbucket" | ||||||
| 
 | 
 | ||||||
|  | 	// CidaasProvider is the provider type for Cidaas IDP
 | ||||||
|  | 	CidaasProvider ProviderType = "cidaas" | ||||||
|  | 
 | ||||||
| 	// DigitalOceanProvider is the provider type for DigitalOcean
 | 	// DigitalOceanProvider is the provider type for DigitalOcean
 | ||||||
| 	DigitalOceanProvider ProviderType = "digitalocean" | 	DigitalOceanProvider ProviderType = "digitalocean" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,144 @@ | ||||||
|  | package providers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/bitly/go-simplejson" | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type GroupsClaimList []GroupClaimEntry | ||||||
|  | 
 | ||||||
|  | type GroupClaimEntry struct { | ||||||
|  | 	GroupID string   `json:"groupId"` | ||||||
|  | 	Roles   []string `json:"roles"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CIDAASProvider represents an CIDAAS based Identity Provider
 | ||||||
|  | type CIDAASProvider struct { | ||||||
|  | 	*OIDCProvider | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var _ Provider = (*CIDAASProvider)(nil) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	CidaasProviderName = "CIDAAS" | ||||||
|  | 	CidaasGroupName    = "cidaas" | ||||||
|  | 	CidaasDefaultScope = "openid email profile roles groups" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // NewCIDAASProvider initiates a new CIDAASProvider
 | ||||||
|  | func NewCIDAASProvider(p *ProviderData, opts options.Provider) *CIDAASProvider { | ||||||
|  | 	p.setProviderDefaults(providerDefaults{ | ||||||
|  | 		name:  CidaasProviderName, | ||||||
|  | 		scope: CidaasDefaultScope, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	return &CIDAASProvider{ | ||||||
|  | 		OIDCProvider: NewOIDCProvider(p, opts.OIDCConfig), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RefreshSession uses the RefreshToken to fetch new Access and ID Tokens
 | ||||||
|  | func (p *CIDAASProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) { | ||||||
|  | 	if s == nil || s.RefreshToken == "" { | ||||||
|  | 		return false, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := p.redeemRefreshToken(ctx, s); err != nil { | ||||||
|  | 		return false, fmt.Errorf("unable to redeem refresh token: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := p.EnrichSession(ctx, s); err != nil { | ||||||
|  | 		return false, fmt.Errorf("unable to enrich session data after refresh: %w %v", err, s) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return true, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // EnrichSession data to add email an groups
 | ||||||
|  | func (p *CIDAASProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error { | ||||||
|  | 	if p.ProfileURL.String() == "" && s.Email == "" { | ||||||
|  | 		return errors.New("id_token did not contain an email and profileURL is not defined") | ||||||
|  | 	} else if p.ProfileURL.String() == "" { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Try to get missing emails or groups from a profileURL
 | ||||||
|  | 	if err := p.enrichFromUserinfoEndpoint(ctx, s); err != nil { | ||||||
|  | 		logger.Errorf("Warning: Profile URL request failed: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// If a mandatory email wasn't set, error at this point.
 | ||||||
|  | 	if s.Email == "" { | ||||||
|  | 		return errors.New("neither the id_token nor the profileURL set an email") | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // enrichFromUserinfoEndpoint enriches a session's Email & Groups via the JSON response of
 | ||||||
|  | // an OIDC profile URL
 | ||||||
|  | func (p *CIDAASProvider) enrichFromUserinfoEndpoint(ctx context.Context, s *sessions.SessionState) error { | ||||||
|  | 	// profile url is userinfo url in case of Cidaas
 | ||||||
|  | 	respJSON, err := requests.New(p.ProfileURL.String()). | ||||||
|  | 		WithContext(ctx). | ||||||
|  | 		WithHeaders(makeOIDCHeader(s.AccessToken)). | ||||||
|  | 		Do(). | ||||||
|  | 		UnmarshalSimpleJSON() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	email, err := respJSON.Get(p.EmailClaim).String() | ||||||
|  | 	if err == nil && s.Email == "" { | ||||||
|  | 		s.Email = email | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	groups, err := p.extractGroups(respJSON) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("extracting groups failed: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	s.Groups = groups | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *CIDAASProvider) extractGroups(respJSON *simplejson.Json) ([]string, error) { | ||||||
|  | 	rawGroupsClaim, err := respJSON.Get(p.GroupsClaim).MarshalJSON() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var groupsClaimList GroupsClaimList | ||||||
|  | 	err = json.Unmarshal(rawGroupsClaim, &groupsClaimList) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var groups []string | ||||||
|  | 	for _, group := range groupsClaimList { | ||||||
|  | 		for _, role := range group.Roles { | ||||||
|  | 			groups = append(groups, fmt.Sprintf("%s:%s", group.GroupID, role)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Cidaas specific roles
 | ||||||
|  | 	if rolesVal, rolesClaimExists := respJSON.CheckGet("roles"); rolesClaimExists { | ||||||
|  | 		cidaasRoles, err := rolesVal.StringArray() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("unmarshal roles failed: %w", err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for _, role := range cidaasRoles { | ||||||
|  | 			groups = append(groups, fmt.Sprintf("%s:%s", CidaasGroupName, role)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return groups, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,493 @@ | ||||||
|  | package providers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"net/url" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" | ||||||
|  | 
 | ||||||
|  | 	"github.com/coreos/go-oidc/v3/oidc" | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 
 | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func newCidaasProvider(serverURL *url.URL) *CIDAASProvider { | ||||||
|  | 	providerData := &ProviderData{ | ||||||
|  | 		ProviderName: "cidaas", | ||||||
|  | 		ClientID:     oidcClientID, | ||||||
|  | 		ClientSecret: oidcSecret, | ||||||
|  | 		LoginURL: &url.URL{ | ||||||
|  | 			Scheme: serverURL.Scheme, | ||||||
|  | 			Host:   serverURL.Host, | ||||||
|  | 			Path:   "/login/oauth/authorize"}, | ||||||
|  | 		RedeemURL: &url.URL{ | ||||||
|  | 			Scheme: serverURL.Scheme, | ||||||
|  | 			Host:   serverURL.Host, | ||||||
|  | 			Path:   "/login/oauth/access_token"}, | ||||||
|  | 		ProfileURL: &url.URL{ | ||||||
|  | 			Scheme: serverURL.Scheme, | ||||||
|  | 			Host:   serverURL.Host, | ||||||
|  | 			Path:   "/profile"}, | ||||||
|  | 		ValidateURL: &url.URL{ | ||||||
|  | 			Scheme: serverURL.Scheme, | ||||||
|  | 			Host:   serverURL.Host, | ||||||
|  | 			Path:   "/api"}, | ||||||
|  | 		Scope:       "openid profile offline_access roles groups", | ||||||
|  | 		EmailClaim:  "email", | ||||||
|  | 		GroupsClaim: "groups", | ||||||
|  | 		Verifier: oidc.NewVerifier( | ||||||
|  | 			oidcIssuer, | ||||||
|  | 			mockJWKS{}, | ||||||
|  | 			&oidc.Config{ClientID: oidcClientID}, | ||||||
|  | 		), | ||||||
|  | 	} | ||||||
|  | 	cfg := options.Provider{ | ||||||
|  | 		Type: options.CidaasProvider, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	p := NewCIDAASProvider(providerData, cfg) | ||||||
|  | 
 | ||||||
|  | 	return p | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func newCidaasServer(pathBodyMap map[string][]byte) (*url.URL, *httptest.Server) { | ||||||
|  | 	s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||||||
|  | 		body, ok := pathBodyMap[r.URL.Path] | ||||||
|  | 		if !ok { | ||||||
|  | 			rw.WriteHeader(404) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		rw.Header().Add("content-type", "application/json") | ||||||
|  | 		_, _ = rw.Write(body) | ||||||
|  | 	})) | ||||||
|  | 	u, _ := url.Parse(s.URL) | ||||||
|  | 	return u, s | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func newTestCidaasSetup(pathToBodyMap map[string][]byte) (*httptest.Server, *CIDAASProvider) { | ||||||
|  | 	redeemURL, server := newCidaasServer(pathToBodyMap) | ||||||
|  | 	provider := newCidaasProvider(redeemURL) | ||||||
|  | 	return server, provider | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestCidaasProvider_EnrichSession(t *testing.T) { | ||||||
|  | 	testCases := map[string]struct { | ||||||
|  | 		ExistingSession *sessions.SessionState | ||||||
|  | 		EmailClaim      string | ||||||
|  | 		GroupsClaim     string | ||||||
|  | 		ProfileJSON     map[string]interface{} | ||||||
|  | 		ExpectedError   error | ||||||
|  | 		ExpectedSession *sessions.SessionState | ||||||
|  | 	}{ | ||||||
|  | 		"Missing Email Only in Profile URL": { | ||||||
|  | 			ExistingSession: &sessions.SessionState{ | ||||||
|  | 				User:         "missing.email", | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 			EmailClaim:  "email", | ||||||
|  | 			GroupsClaim: "groups", | ||||||
|  | 			ProfileJSON: map[string]interface{}{ | ||||||
|  | 				"email": "found@email.com", | ||||||
|  | 			}, | ||||||
|  | 			ExpectedError: nil, | ||||||
|  | 			ExpectedSession: &sessions.SessionState{ | ||||||
|  | 				User:         "missing.email", | ||||||
|  | 				Email:        "found@email.com", | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		"Missing Email with Custom Claim": { | ||||||
|  | 			ExistingSession: &sessions.SessionState{ | ||||||
|  | 				User:         "missing.email", | ||||||
|  | 				Groups:       []string{"already", "populated"}, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 			EmailClaim:  "weird", | ||||||
|  | 			GroupsClaim: "groups", | ||||||
|  | 			ProfileJSON: map[string]interface{}{ | ||||||
|  | 				"weird": "weird@claim.com", | ||||||
|  | 				"groups": []map[string]interface{}{ | ||||||
|  | 					{ | ||||||
|  | 						"groupId": "CIDAAS_USERS", | ||||||
|  | 						"roles":   []string{"USER"}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				"roles": []string{"USER"}, | ||||||
|  | 			}, | ||||||
|  | 			ExpectedError: nil, | ||||||
|  | 			ExpectedSession: &sessions.SessionState{ | ||||||
|  | 				User:         "missing.email", | ||||||
|  | 				Email:        "weird@claim.com", | ||||||
|  | 				Groups:       []string{"CIDAAS_USERS:USER", "cidaas:USER"}, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		"Missing Email not in Profile URL": { | ||||||
|  | 			ExistingSession: &sessions.SessionState{ | ||||||
|  | 				User:         "missing.email", | ||||||
|  | 				Groups:       []string{"already", "populated"}, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 			EmailClaim:  "email", | ||||||
|  | 			GroupsClaim: "groups", | ||||||
|  | 			ProfileJSON: map[string]interface{}{ | ||||||
|  | 				"groups": []map[string]interface{}{ | ||||||
|  | 					{ | ||||||
|  | 						"groupId": "CIDAAS_USERS", | ||||||
|  | 						"roles":   []string{"USER"}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				"roles": []string{"USER"}, | ||||||
|  | 			}, | ||||||
|  | 			ExpectedError: errors.New("neither the id_token nor the profileURL set an email"), | ||||||
|  | 			ExpectedSession: &sessions.SessionState{ | ||||||
|  | 				User:         "missing.email", | ||||||
|  | 				Groups:       []string{"CIDAAS_USERS:USER", "cidaas:USER"}, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		"Missing Groups": { | ||||||
|  | 			ExistingSession: &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "already@populated.com", | ||||||
|  | 				Groups:       nil, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 			EmailClaim:  "email", | ||||||
|  | 			GroupsClaim: "groups", | ||||||
|  | 			ProfileJSON: map[string]interface{}{ | ||||||
|  | 				"email": "new@thing.com", | ||||||
|  | 				"groups": []map[string]interface{}{ | ||||||
|  | 					{ | ||||||
|  | 						"groupId": "CIDAAS_USERS", | ||||||
|  | 						"roles":   []string{"USER"}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				"roles": []string{"USER"}, | ||||||
|  | 			}, | ||||||
|  | 			ExpectedError: nil, | ||||||
|  | 			ExpectedSession: &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "already@populated.com", | ||||||
|  | 				Groups:       []string{"CIDAAS_USERS:USER", "cidaas:USER"}, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		"Empty Groups Claims": { | ||||||
|  | 			ExistingSession: &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "already@populated.com", | ||||||
|  | 				Groups:       []string{}, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 			EmailClaim:  "email", | ||||||
|  | 			GroupsClaim: "groups", | ||||||
|  | 			ProfileJSON: map[string]interface{}{ | ||||||
|  | 				"email": "new@thing.com", | ||||||
|  | 				"groups": []map[string]interface{}{ | ||||||
|  | 					{ | ||||||
|  | 						"groupId": "CIDAAS_USERS", | ||||||
|  | 						"roles":   []string{"USER"}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				"roles": []string{"USER"}, | ||||||
|  | 			}, | ||||||
|  | 			ExpectedError: nil, | ||||||
|  | 			ExpectedSession: &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "already@populated.com", | ||||||
|  | 				Groups:       []string{"CIDAAS_USERS:USER", "cidaas:USER"}, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		"Missing Groups with Custom Claim": { | ||||||
|  | 			ExistingSession: &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "already@populated.com", | ||||||
|  | 				Groups:       nil, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 			EmailClaim:  "email", | ||||||
|  | 			GroupsClaim: "groups2", | ||||||
|  | 			ProfileJSON: map[string]interface{}{ | ||||||
|  | 				"email": "already@populated.com", | ||||||
|  | 				"groups2": []map[string]interface{}{ | ||||||
|  | 					{ | ||||||
|  | 						"sub":     "aa5181ea-0841-4ea7-b67f-81882f153d40", | ||||||
|  | 						"groupId": "CIDAAS_ADMINS", | ||||||
|  | 						"path":    "/CIDAAS_ADMINS/", | ||||||
|  | 						"roles":   []string{"ADMIN"}, | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						"sub":       "aa5181ea-0841-4ea7-b67f-81882f153d39", | ||||||
|  | 						"groupId":   "customers", | ||||||
|  | 						"groupType": "Customers", | ||||||
|  | 						"path":      "/customers/", | ||||||
|  | 						"roles": []string{ | ||||||
|  | 							"CUSTOMER_ACCOUNT_LOGIN", | ||||||
|  | 							"GROUP_ADMIN", | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						"groupId": "CIDAAS_USERS", | ||||||
|  | 						"roles":   []string{"USER"}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				"roles": []string{"USER"}, | ||||||
|  | 			}, | ||||||
|  | 			ExpectedError: nil, | ||||||
|  | 			ExpectedSession: &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "already@populated.com", | ||||||
|  | 				Groups:       []string{"CIDAAS_ADMINS:ADMIN", "customers:CUSTOMER_ACCOUNT_LOGIN", "customers:GROUP_ADMIN", "CIDAAS_USERS:USER", "cidaas:USER"}, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		"Just format Groups": { | ||||||
|  | 			ExistingSession: &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "already@populated.com", | ||||||
|  | 				Groups:       nil, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 			EmailClaim:  "email", | ||||||
|  | 			GroupsClaim: "groups2", | ||||||
|  | 			ProfileJSON: map[string]interface{}{ | ||||||
|  | 				"email": "already@populated.com", | ||||||
|  | 				"groups2": []map[string]interface{}{ | ||||||
|  | 					{ | ||||||
|  | 						"sub":       "aa5181ea-0841-4ea7-b67f-81882f153d39", | ||||||
|  | 						"groupId":   "customers", | ||||||
|  | 						"groupType": "Customers", | ||||||
|  | 						"path":      "/customers/", | ||||||
|  | 						"roles": []string{ | ||||||
|  | 							"CUSTOMER_ACCOUNT_LOGIN", | ||||||
|  | 							"GROUP_ADMIN", | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						"groupId": "CIDAAS_USERS", | ||||||
|  | 						"roles":   []string{"USER"}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				"roles": []string{"USER"}, | ||||||
|  | 			}, | ||||||
|  | 			ExpectedError: nil, | ||||||
|  | 			ExpectedSession: &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "already@populated.com", | ||||||
|  | 				Groups:       []string{"customers:CUSTOMER_ACCOUNT_LOGIN", "customers:GROUP_ADMIN", "CIDAAS_USERS:USER", "cidaas:USER"}, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		"Missing Groups String Profile URL Response": { | ||||||
|  | 			ExistingSession: &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "already@populated.com", | ||||||
|  | 				Groups:       nil, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 			EmailClaim:  "email", | ||||||
|  | 			GroupsClaim: "groups", | ||||||
|  | 			ProfileJSON: map[string]interface{}{ | ||||||
|  | 				"groups": []map[string]interface{}{ | ||||||
|  | 					{ | ||||||
|  | 						"sub":     "aa5181ea-0841-4ea7-b67f-81882f153d40", | ||||||
|  | 						"groupId": "CIDAAS_ADMINS", | ||||||
|  | 						"path":    "/CIDAAS_ADMINS/", | ||||||
|  | 						"roles":   []string{"ADMIN"}, | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						"sub":       "aa5181ea-0841-4ea7-b67f-81882f153d39", | ||||||
|  | 						"groupId":   "customers", | ||||||
|  | 						"groupType": "Customers", | ||||||
|  | 						"path":      "/customers/", | ||||||
|  | 						"roles": []string{ | ||||||
|  | 							"CUSTOMER_ACCOUNT_LOGIN", | ||||||
|  | 							"GROUP_ADMIN", | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						"groupId": "CIDAAS_USERS", | ||||||
|  | 						"roles":   []string{"USER"}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				"roles": []string{"USER"}, | ||||||
|  | 			}, | ||||||
|  | 			ExpectedError: nil, | ||||||
|  | 			ExpectedSession: &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "already@populated.com", | ||||||
|  | 				Groups:       []string{"CIDAAS_ADMINS:ADMIN", "customers:CUSTOMER_ACCOUNT_LOGIN", "customers:GROUP_ADMIN", "CIDAAS_USERS:USER", "cidaas:USER"}, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		"Missing Groups in both Claims and Profile URL": { | ||||||
|  | 			ExistingSession: &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "already@populated.com", | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 			EmailClaim:  "email", | ||||||
|  | 			GroupsClaim: "groups", | ||||||
|  | 			ProfileJSON: map[string]interface{}{ | ||||||
|  | 				"email": "new@thing.com", | ||||||
|  | 			}, | ||||||
|  | 			ExpectedError: nil, | ||||||
|  | 			ExpectedSession: &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "already@populated.com", | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for testName, tc := range testCases { | ||||||
|  | 		t.Run(testName, func(t *testing.T) { | ||||||
|  | 			path := "/userinfo/" | ||||||
|  | 			jsonResp, err := json.Marshal(tc.ProfileJSON) | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 
 | ||||||
|  | 			server, provider := newTestCidaasSetup(map[string][]byte{path: jsonResp}) | ||||||
|  | 			provider.ProfileURL, err = url.Parse(fmt.Sprintf("%s%s", server.URL, path)) | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 
 | ||||||
|  | 			provider.EmailClaim = tc.EmailClaim | ||||||
|  | 			provider.GroupsClaim = tc.GroupsClaim | ||||||
|  | 			defer server.Close() | ||||||
|  | 
 | ||||||
|  | 			err = provider.EnrichSession(context.Background(), tc.ExistingSession) | ||||||
|  | 			if tc.ExpectedError != nil { | ||||||
|  | 				assert.EqualError(t, err, tc.ExpectedError.Error()) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 			} | ||||||
|  | 			assert.Equal(t, *tc.ExpectedSession, *tc.ExistingSession) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestCidaasProvider_RefreshSession(t *testing.T) { | ||||||
|  | 	testCases := map[string]struct { | ||||||
|  | 		ExistingSession   *sessions.SessionState | ||||||
|  | 		EmailClaim        string | ||||||
|  | 		GroupsClaim       string | ||||||
|  | 		ProfileJSON       map[string]interface{} | ||||||
|  | 		RedeemJSON        redeemTokenResponse | ||||||
|  | 		ExpectedRefreshed bool | ||||||
|  | 		ExpectedError     error | ||||||
|  | 		ExpectedEmail     string | ||||||
|  | 		ExpectedUser      string | ||||||
|  | 	}{ | ||||||
|  | 		"Refresh session successfully": { | ||||||
|  | 			ExistingSession: &sessions.SessionState{ | ||||||
|  | 				User:         "session.is.not.locked", | ||||||
|  | 				Email:        "found@email.com", | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 			RedeemJSON: redeemTokenResponse{ | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				ExpiresIn:    10, | ||||||
|  | 				TokenType:    "Bearer", | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 			ExpectedRefreshed: true, | ||||||
|  | 			ExpectedError:     nil, | ||||||
|  | 			ExpectedEmail:     defaultIDToken.Email, | ||||||
|  | 			ExpectedUser:      defaultIDToken.Subject, | ||||||
|  | 		}, | ||||||
|  | 		"Unable to refresh session": { | ||||||
|  | 			ExistingSession: &sessions.SessionState{ | ||||||
|  | 				User:         "session.is.unable.to.refresh", | ||||||
|  | 				Email:        "found@email.com", | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  accessToken, | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			}, | ||||||
|  | 			ExpectedRefreshed: false, | ||||||
|  | 			ExpectedError:     fmt.Errorf("unable to redeem refresh token: failed to get token: oauth2: server response missing access_token"), | ||||||
|  | 			ExpectedUser:      "session.is.unable.to.refresh", | ||||||
|  | 			ExpectedEmail:     "found@email.com", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for testName, tc := range testCases { | ||||||
|  | 		t.Run(testName, func(t *testing.T) { | ||||||
|  | 			idToken, _ := newSignedTestIDToken(defaultIDToken) | ||||||
|  | 			tc.RedeemJSON.IDToken = idToken | ||||||
|  | 			redeemPath := "/token/" | ||||||
|  | 			redeemJSONResp, err := json.Marshal(tc.RedeemJSON) | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 
 | ||||||
|  | 			serverURL, server := newCidaasServer( | ||||||
|  | 				map[string][]byte{ | ||||||
|  | 					redeemPath: redeemJSONResp, | ||||||
|  | 				}) | ||||||
|  | 			provider := newCidaasProvider(serverURL) | ||||||
|  | 
 | ||||||
|  | 			// Disable session enrichment, because we want to focus on refreshing logic
 | ||||||
|  | 			provider.ProfileURL, err = url.Parse("") | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 			provider.RedeemURL, err = url.Parse(fmt.Sprintf("%s%s", server.URL, redeemPath)) | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 
 | ||||||
|  | 			provider.GroupsClaim = tc.GroupsClaim | ||||||
|  | 			defer server.Close() | ||||||
|  | 
 | ||||||
|  | 			var refreshed bool | ||||||
|  | 			refreshed, err = provider.RefreshSession(context.Background(), tc.ExistingSession) | ||||||
|  | 
 | ||||||
|  | 			if tc.ExpectedError != nil { | ||||||
|  | 				assert.EqualError(t, err, tc.ExpectedError.Error()) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 			} | ||||||
|  | 			assert.Equal(t, tc.ExpectedRefreshed, refreshed) | ||||||
|  | 			assert.Equal(t, tc.ExpectedEmail, tc.ExistingSession.Email) | ||||||
|  | 			assert.Equal(t, tc.ExpectedUser, tc.ExistingSession.User) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -45,6 +45,8 @@ func NewProvider(providerConfig options.Provider) (Provider, error) { | ||||||
| 		return NewMicrosoftEntraIDProvider(providerData, providerConfig), nil | 		return NewMicrosoftEntraIDProvider(providerData, providerConfig), nil | ||||||
| 	case options.BitbucketProvider: | 	case options.BitbucketProvider: | ||||||
| 		return NewBitbucketProvider(providerData, providerConfig.BitbucketConfig), nil | 		return NewBitbucketProvider(providerData, providerConfig.BitbucketConfig), nil | ||||||
|  | 	case options.CidaasProvider: | ||||||
|  | 		return NewCIDAASProvider(providerData, providerConfig), nil | ||||||
| 	case options.DigitalOceanProvider: | 	case options.DigitalOceanProvider: | ||||||
| 		return NewDigitalOceanProvider(providerData), nil | 		return NewDigitalOceanProvider(providerData), nil | ||||||
| 	case options.FacebookProvider: | 	case options.FacebookProvider: | ||||||
|  | @ -188,7 +190,8 @@ func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bo | ||||||
| 		options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider, | 		options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider, | ||||||
| 		options.NextCloudProvider, options.SourceHutProvider: | 		options.NextCloudProvider, options.SourceHutProvider: | ||||||
| 		return false, nil | 		return false, nil | ||||||
| 	case options.ADFSProvider, options.AzureProvider, options.GitLabProvider, options.KeycloakOIDCProvider, options.OIDCProvider, options.MicrosoftEntraIDProvider: | 	case options.OIDCProvider, options.ADFSProvider, options.AzureProvider, options.CidaasProvider, | ||||||
|  | 		options.GitLabProvider, options.KeycloakOIDCProvider, options.MicrosoftEntraIDProvider: | ||||||
| 		return true, nil | 		return true, nil | ||||||
| 	default: | 	default: | ||||||
| 		return false, fmt.Errorf("unknown provider type: %s", providerType) | 		return false, fmt.Errorf("unknown provider type: %s", providerType) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue