Merge pull request #1210 from pb82/keycloak-oidc-provider
Keycloak oidc provider
This commit is contained in:
		
						commit
						526aff8c84
					
				|  | @ -9,6 +9,8 @@ | ||||||
|   to a version before this first and allow legacy sessions to expire gracefully or change your `cookie-secret` |   to a version before this first and allow legacy sessions to expire gracefully or change your `cookie-secret` | ||||||
|   value and force all sessions to reauthenticate. |   value and force all sessions to reauthenticate. | ||||||
|    |    | ||||||
|  | - [#1210](https://github.com/oauth2-proxy/oauth2-proxy/pull/1210) A new `keycloak-oidc` provider has been added with support for role based authentication. The existing keycloak auth provider will eventually be deprecated and removed. Please switch to the new provider `keycloak-oidc`. | ||||||
|  | 
 | ||||||
| ## Breaking Changes | ## Breaking Changes | ||||||
| 
 | 
 | ||||||
| ## Changes since v7.1.3 | ## Changes since v7.1.3 | ||||||
|  | @ -27,6 +29,7 @@ | ||||||
| - [#1142](https://github.com/oauth2-proxy/oauth2-proxy/pull/1142) Add pagewriter to upstream proxy (@JoelSpeed) | - [#1142](https://github.com/oauth2-proxy/oauth2-proxy/pull/1142) Add pagewriter to upstream proxy (@JoelSpeed) | ||||||
| - [#1181](https://github.com/oauth2-proxy/oauth2-proxy/pull/1181) Fix incorrect `cfg` name in show-debug-on-error flag (@iTaybb) | - [#1181](https://github.com/oauth2-proxy/oauth2-proxy/pull/1181) Fix incorrect `cfg` name in show-debug-on-error flag (@iTaybb) | ||||||
| - [#1207](https://github.com/oauth2-proxy/oauth2-proxy/pull/1207) Fix URI fragment handling on sign-in page, regression introduced in 7.1.0 (@tarvip) | - [#1207](https://github.com/oauth2-proxy/oauth2-proxy/pull/1207) Fix URI fragment handling on sign-in page, regression introduced in 7.1.0 (@tarvip) | ||||||
|  | - [#1210](https://github.com/oauth2-proxy/oauth2-proxy/pull/1210) New Keycloak OIDC Provider (@pb82) | ||||||
| - [#1244](https://github.com/oauth2-proxy/oauth2-proxy/pull/1244) Update Alpine image version to 3.14 (@ahovgaard) | - [#1244](https://github.com/oauth2-proxy/oauth2-proxy/pull/1244) Update Alpine image version to 3.14 (@ahovgaard) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -250,6 +250,7 @@ make up the header value | ||||||
| | Field | Type | Description | | | Field | Type | Description | | ||||||
| | ----- | ---- | ----------- | | | ----- | ---- | ----------- | | ||||||
| | `groups` | _[]string_ | Group enables to restrict login to members of indicated group | | | `groups` | _[]string_ | Group enables to restrict login to members of indicated group | | ||||||
|  | | `roles` | _[]string_ | Role enables to restrict login to users with role (only available when using the keycloak-oidc provider) | | ||||||
| 
 | 
 | ||||||
| ### LoginGovOptions | ### LoginGovOptions | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -146,12 +146,15 @@ If you are using GitHub enterprise, make sure you set the following to the appro | ||||||
| 
 | 
 | ||||||
| ### Keycloak Auth Provider | ### Keycloak Auth Provider | ||||||
| 
 | 
 | ||||||
| 1.  Create new client in your Keycloak with **Access Type** 'confidental' and **Valid Redirect URIs** 'https://internal.yourcompany.com/oauth2/callback' | 1.  Create new client in your Keycloak realm with **Access Type** 'confidental' and **Valid Redirect URIs** 'https://internal.yourcompany.com/oauth2/callback' | ||||||
| 2.  Take note of the Secret in the credential tab of the client | 2.  Take note of the Secret in the credential tab of the client | ||||||
| 3.  Create a mapper with **Mapper Type** 'Group Membership' and **Token Claim Name** 'groups'. | 3.  Create a mapper with **Mapper Type** 'Group Membership' and **Token Claim Name** 'groups'. | ||||||
| 
 | 
 | ||||||
|  | :::note this is the legacy Keycloak Auth Prodiver, use `keycloak-oidc` if possible. ::: | ||||||
|  | 
 | ||||||
| Make sure you set the following to the appropriate url: | Make sure you set the following to the appropriate url: | ||||||
| 
 | 
 | ||||||
|  | ``` | ||||||
|     --provider=keycloak |     --provider=keycloak | ||||||
|     --client-id=<client you have created> |     --client-id=<client you have created> | ||||||
|     --client-secret=<your client's secret> |     --client-secret=<your client's secret> | ||||||
|  | @ -161,6 +164,7 @@ Make sure you set the following to the appropriate url: | ||||||
|     --validate-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/userinfo" |     --validate-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/userinfo" | ||||||
|     --keycloak-group=<first_allowed_user_group> |     --keycloak-group=<first_allowed_user_group> | ||||||
|     --keycloak-group=<second_allowed_user_group> |     --keycloak-group=<second_allowed_user_group> | ||||||
|  | ``` | ||||||
|      |      | ||||||
| For group based authorization, the optional `--keycloak-group` (legacy) or `--allowed-group` (global standard) | For group based authorization, the optional `--keycloak-group` (legacy) or `--allowed-group` (global standard) | ||||||
| flags can be used to specify which groups to limit access to. | flags can be used to specify which groups to limit access to. | ||||||
|  | @ -172,6 +176,25 @@ Keycloak userinfo endpoint response. | ||||||
| The group management in keycloak is using a tree. If you create a group named admin in keycloak | The group management in keycloak is using a tree. If you create a group named admin in keycloak | ||||||
| you should define the 'keycloak-group' value to /admin. | you should define the 'keycloak-group' value to /admin. | ||||||
| 
 | 
 | ||||||
|  | ### Keycloak OIDC Auth Provider | ||||||
|  | 
 | ||||||
|  | 1.  Create new client in your Keycloak realm with **Access Type** 'confidental', **Client protocol**  'openid-connect' and **Valid Redirect URIs** 'https://internal.yourcompany.com/oauth2/callback' | ||||||
|  | 2.  Take note of the Secret in the credential tab of the client | ||||||
|  | 3.  Create a mapper with **Mapper Type** 'Group Membership' and **Token Claim Name** 'groups'. | ||||||
|  | 4.  Create a mapper with **Mapper Type** 'Audience' and **Included Client Audience** and **Included Custom Audience** set to your client name. | ||||||
|  | 
 | ||||||
|  | Make sure you set the following to the appropriate url: | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  |     --provider=keycloak-oidc | ||||||
|  |     --client-id=<your client's id> | ||||||
|  |     --client-secret=<your client's secret> | ||||||
|  |     --redirect-url=https://myapp.com/oauth2/callback | ||||||
|  |     --oidc-issuer-url=https://<keycloak host>/auth/<your realm>/basic | ||||||
|  |     --allowed-role=<realm role name> // Optional, required realm role | ||||||
|  |     --allowed-role=<client id>:<client role name> // Optional, required client role | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
| ### GitLab Auth Provider | ### GitLab Auth Provider | ||||||
| 
 | 
 | ||||||
| This auth provider has been tested against Gitlab version 12.X. Due to Gitlab API changes, it may not work for version prior to 12.X (see [994](https://github.com/oauth2-proxy/oauth2-proxy/issues/994)). | This auth provider has been tested against Gitlab version 12.X. Due to Gitlab API changes, it may not work for version prior to 12.X (see [994](https://github.com/oauth2-proxy/oauth2-proxy/issues/994)). | ||||||
|  |  | ||||||
|  | @ -192,6 +192,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/ | ||||||
| | `--tls-key-file` | string | path to private key file | | | | `--tls-key-file` | string | path to private key file | | | ||||||
| | `--upstream` | string \| list | the http url(s) of the upstream endpoint, file:// paths for static files or `static://<status_code>` for static response. Routing is based on the path | | | | `--upstream` | string \| list | the http url(s) of the upstream endpoint, file:// paths for static files or `static://<status_code>` for static response. Routing is based on the path | | | ||||||
| | `--allowed-group` | string \| list | restrict logins to members of this group (may be given multiple times) | | | | `--allowed-group` | string \| list | restrict logins to members of this group (may be given multiple times) | | | ||||||
|  | | `--allowed-role` | string \| list | restrict logins to users with this role (may be given multiple times). Only works with the keycloak-oidc provider. | | | ||||||
| | `--validate-url` | string | Access token validation endpoint | | | | `--validate-url` | string | Access token validation endpoint | | | ||||||
| | `--version` | n/a | print version string | | | | `--version` | n/a | print version string | | | ||||||
| | `--whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (e.g. `.example.com`) \[[2](#footnote2)\] | | | | `--whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (e.g. `.example.com`) \[[2](#footnote2)\] | | | ||||||
|  |  | ||||||
|  | @ -508,6 +508,7 @@ type LegacyProvider struct { | ||||||
| 	ApprovalPrompt                     string   `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0
 | 	ApprovalPrompt                     string   `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0
 | ||||||
| 	UserIDClaim                        string   `flag:"user-id-claim" cfg:"user_id_claim"` | 	UserIDClaim                        string   `flag:"user-id-claim" cfg:"user_id_claim"` | ||||||
| 	AllowedGroups                      []string `flag:"allowed-group" cfg:"allowed_groups"` | 	AllowedGroups                      []string `flag:"allowed-group" cfg:"allowed_groups"` | ||||||
|  | 	AllowedRoles                       []string `flag:"allowed-role" cfg:"allowed_roles"` | ||||||
| 
 | 
 | ||||||
| 	AcrValues  string `flag:"acr-values" cfg:"acr_values"` | 	AcrValues  string `flag:"acr-values" cfg:"acr_values"` | ||||||
| 	JWTKey     string `flag:"jwt-key" cfg:"jwt_key"` | 	JWTKey     string `flag:"jwt-key" cfg:"jwt_key"` | ||||||
|  | @ -563,6 +564,7 @@ func legacyProviderFlagSet() *pflag.FlagSet { | ||||||
| 
 | 
 | ||||||
| 	flagSet.String("user-id-claim", providers.OIDCEmailClaim, "(DEPRECATED for `oidc-email-claim`) which claim contains the user ID") | 	flagSet.String("user-id-claim", providers.OIDCEmailClaim, "(DEPRECATED for `oidc-email-claim`) which claim contains the user ID") | ||||||
| 	flagSet.StringSlice("allowed-group", []string{}, "restrict logins to members of this group (may be given multiple times)") | 	flagSet.StringSlice("allowed-group", []string{}, "restrict logins to members of this group (may be given multiple times)") | ||||||
|  | 	flagSet.StringSlice("allowed-role", []string{}, "(keycloak-oidc) restrict logins to members of these roles (may be given multiple times)") | ||||||
| 
 | 
 | ||||||
| 	return flagSet | 	return flagSet | ||||||
| } | } | ||||||
|  | @ -656,6 +658,11 @@ func (l *LegacyProvider) convert() (Providers, error) { | ||||||
| 			Token: l.GitHubToken, | 			Token: l.GitHubToken, | ||||||
| 			Users: l.GitHubUsers, | 			Users: l.GitHubUsers, | ||||||
| 		} | 		} | ||||||
|  | 	case "keycloak-oidc": | ||||||
|  | 		provider.KeycloakConfig = KeycloakOptions{ | ||||||
|  | 			Groups: l.KeycloakGroups, | ||||||
|  | 			Roles:  l.AllowedRoles, | ||||||
|  | 		} | ||||||
| 	case "keycloak": | 	case "keycloak": | ||||||
| 		provider.KeycloakConfig = KeycloakOptions{ | 		provider.KeycloakConfig = KeycloakOptions{ | ||||||
| 			Groups: l.KeycloakGroups, | 			Groups: l.KeycloakGroups, | ||||||
|  |  | ||||||
|  | @ -78,6 +78,9 @@ type Provider struct { | ||||||
| type KeycloakOptions struct { | type KeycloakOptions struct { | ||||||
| 	// Group enables to restrict login to members of indicated group
 | 	// Group enables to restrict login to members of indicated group
 | ||||||
| 	Groups []string `json:"groups,omitempty"` | 	Groups []string `json:"groups,omitempty"` | ||||||
|  | 
 | ||||||
|  | 	// Role enables to restrict login to users with role (only available when using the keycloak-oidc provider)
 | ||||||
|  | 	Roles []string `json:"roles,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type AzureOptions struct { | type AzureOptions struct { | ||||||
|  |  | ||||||
|  | @ -247,6 +247,11 @@ func parseProviderInfo(o *options.Options, msgs []string) []string { | ||||||
| 		if len(o.Providers[0].KeycloakConfig.Groups) > 0 { | 		if len(o.Providers[0].KeycloakConfig.Groups) > 0 { | ||||||
| 			p.SetAllowedGroups(o.Providers[0].KeycloakConfig.Groups) | 			p.SetAllowedGroups(o.Providers[0].KeycloakConfig.Groups) | ||||||
| 		} | 		} | ||||||
|  | 	case *providers.KeycloakOIDCProvider: | ||||||
|  | 		if p.Verifier == nil { | ||||||
|  | 			msgs = append(msgs, "keycloak-oidc provider requires an oidc issuer URL") | ||||||
|  | 		} | ||||||
|  | 		p.AddAllowedRoles(o.Providers[0].KeycloakConfig.Roles) | ||||||
| 	case *providers.GoogleProvider: | 	case *providers.GoogleProvider: | ||||||
| 		if o.Providers[0].GoogleConfig.ServiceAccountJSON != "" { | 		if o.Providers[0].GoogleConfig.ServiceAccountJSON != "" { | ||||||
| 			file, err := os.Open(o.Providers[0].GoogleConfig.ServiceAccountJSON) | 			file, err := os.Open(o.Providers[0].GoogleConfig.ServiceAccountJSON) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,142 @@ | ||||||
|  | package providers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const keycloakOIDCProviderName = "Keycloak OIDC" | ||||||
|  | 
 | ||||||
|  | // KeycloakOIDCProvider creates a Keycloak provider based on OIDCProvider
 | ||||||
|  | type KeycloakOIDCProvider struct { | ||||||
|  | 	*OIDCProvider | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewKeycloakOIDCProvider makes a KeycloakOIDCProvider using the ProviderData
 | ||||||
|  | func NewKeycloakOIDCProvider(p *ProviderData) *KeycloakOIDCProvider { | ||||||
|  | 	p.ProviderName = keycloakOIDCProviderName | ||||||
|  | 	return &KeycloakOIDCProvider{ | ||||||
|  | 		OIDCProvider: &OIDCProvider{ | ||||||
|  | 			ProviderData: p, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var _ Provider = (*KeycloakOIDCProvider)(nil) | ||||||
|  | 
 | ||||||
|  | // AddAllowedRoles sets Keycloak roles that are authorized.
 | ||||||
|  | // Assumes `SetAllowedGroups` is already called on groups and appends to that
 | ||||||
|  | // with `role:` prefixed roles.
 | ||||||
|  | func (p *KeycloakOIDCProvider) AddAllowedRoles(roles []string) { | ||||||
|  | 	if p.AllowedGroups == nil { | ||||||
|  | 		p.AllowedGroups = make(map[string]struct{}) | ||||||
|  | 	} | ||||||
|  | 	for _, role := range roles { | ||||||
|  | 		p.AllowedGroups[formatRole(role)] = struct{}{} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // EnrichSession is called after Redeem to allow providers to enrich session fields
 | ||||||
|  | // such as User, Email, Groups with provider specific API calls.
 | ||||||
|  | func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error { | ||||||
|  | 	err := p.OIDCProvider.EnrichSession(ctx, s) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("could not enrich oidc session: %v", err) | ||||||
|  | 	} | ||||||
|  | 	return p.extractRoles(ctx, s) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RefreshSession adds role extraction logic to the refresh flow
 | ||||||
|  | func (p *KeycloakOIDCProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) { | ||||||
|  | 	refreshed, err := p.OIDCProvider.RefreshSession(ctx, s) | ||||||
|  | 
 | ||||||
|  | 	// Refresh could have failed or there was not session to refresh (with no error raised)
 | ||||||
|  | 	if err != nil || !refreshed { | ||||||
|  | 		return refreshed, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return true, p.extractRoles(ctx, s) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *KeycloakOIDCProvider) extractRoles(ctx context.Context, s *sessions.SessionState) error { | ||||||
|  | 	claims, err := p.getAccessClaims(ctx, s) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var roles []string | ||||||
|  | 	roles = append(roles, claims.RealmAccess.Roles...) | ||||||
|  | 	roles = append(roles, getClientRoles(claims)...) | ||||||
|  | 
 | ||||||
|  | 	// Add to groups list with `role:` prefix to distinguish from groups
 | ||||||
|  | 	for _, role := range roles { | ||||||
|  | 		s.Groups = append(s.Groups, formatRole(role)) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type realmAccess struct { | ||||||
|  | 	Roles []string `json:"roles"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type accessClaims struct { | ||||||
|  | 	RealmAccess    realmAccess            `json:"realm_access"` | ||||||
|  | 	ResourceAccess map[string]interface{} `json:"resource_access"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *KeycloakOIDCProvider) getAccessClaims(ctx context.Context, s *sessions.SessionState) (*accessClaims, error) { | ||||||
|  | 	// HACK: This isn't an ID Token, but has similar structure & signing
 | ||||||
|  | 	token, err := p.Verifier.Verify(ctx, s.AccessToken) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var claims *accessClaims | ||||||
|  | 	if err = token.Claims(&claims); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return claims, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // getClientRoles extracts client roles from the `resource_access` claim with
 | ||||||
|  | // the format `client:role`.
 | ||||||
|  | //
 | ||||||
|  | // ResourceAccess format:
 | ||||||
|  | // "resource_access": {
 | ||||||
|  | //   "clientA": {
 | ||||||
|  | //     "roles": [
 | ||||||
|  | //       "roleA"
 | ||||||
|  | //     ]
 | ||||||
|  | //   },
 | ||||||
|  | //   "clientB": {
 | ||||||
|  | //     "roles": [
 | ||||||
|  | //       "roleA",
 | ||||||
|  | //       "roleB",
 | ||||||
|  | //       "roleC"
 | ||||||
|  | //     ]
 | ||||||
|  | //   }
 | ||||||
|  | // }
 | ||||||
|  | func getClientRoles(claims *accessClaims) []string { | ||||||
|  | 	var clientRoles []string | ||||||
|  | 	for clientName, access := range claims.ResourceAccess { | ||||||
|  | 		accessMap, ok := access.(map[string]interface{}) | ||||||
|  | 		if !ok { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var roles interface{} | ||||||
|  | 		if roles, ok = accessMap["roles"]; !ok { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		for _, role := range roles.([]interface{}) { | ||||||
|  | 			clientRoles = append(clientRoles, fmt.Sprintf("%s:%s", clientName, role)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return clientRoles | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func formatRole(role string) string { | ||||||
|  | 	return fmt.Sprintf("role:%s", role) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,189 @@ | ||||||
|  | package providers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"net/url" | ||||||
|  | 
 | ||||||
|  | 	"github.com/coreos/go-oidc/v3/oidc" | ||||||
|  | 
 | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" | ||||||
|  | 	. "github.com/onsi/ginkgo" | ||||||
|  | 	. "github.com/onsi/gomega" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	accessTokenHeader    = "ewogICJhbGciOiAiUlMyNTYiLAogICJ0eXAiOiAiSldUIgp9" | ||||||
|  | 	accessTokenPayload   = "eyJyZWFsbV9hY2Nlc3MiOiB7InJvbGVzIjogWyJ3cml0ZSJdfSwgInJlc291cmNlX2FjY2VzcyI6IHsiZGVmYXVsdCI6IHsicm9sZXMiOiBbInJlYWQiXX19fQ" | ||||||
|  | 	accessTokenSignature = "dyt0CoTl4WoVjAHI9Q_CwSKhl6d_9rhM3NrXuJttkao" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type DummyKeySet struct{} | ||||||
|  | 
 | ||||||
|  | func (DummyKeySet) VerifySignature(_ context.Context, _ string) (payload []byte, err error) { | ||||||
|  | 	p, _ := base64.RawURLEncoding.DecodeString(accessTokenPayload) | ||||||
|  | 	return p, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getAccessToken() string { | ||||||
|  | 	return fmt.Sprintf("%s.%s.%s", accessTokenHeader, accessTokenPayload, accessTokenSignature) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func newTestKeycloakOIDCSetup() (*httptest.Server, *KeycloakOIDCProvider) { | ||||||
|  | 	redeemURL, server := newOIDCServer([]byte(fmt.Sprintf(`{"email": "new@thing.com", "expires_in": 300, "access_token": "%v"}`, getAccessToken()))) | ||||||
|  | 	provider := newKeycloakOIDCProvider(redeemURL) | ||||||
|  | 	return server, provider | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func newKeycloakOIDCProvider(serverURL *url.URL) *KeycloakOIDCProvider { | ||||||
|  | 	p := NewKeycloakOIDCProvider( | ||||||
|  | 		&ProviderData{ | ||||||
|  | 			LoginURL: &url.URL{ | ||||||
|  | 				Scheme: "https", | ||||||
|  | 				Host:   "keycloak-oidc.com", | ||||||
|  | 				Path:   "/oauth/auth"}, | ||||||
|  | 			RedeemURL: &url.URL{ | ||||||
|  | 				Scheme: "https", | ||||||
|  | 				Host:   "keycloak-oidc.com", | ||||||
|  | 				Path:   "/oauth/token"}, | ||||||
|  | 			ProfileURL: &url.URL{ | ||||||
|  | 				Scheme: "https", | ||||||
|  | 				Host:   "keycloak-oidc.com", | ||||||
|  | 				Path:   "/api/v3/user"}, | ||||||
|  | 			ValidateURL: &url.URL{ | ||||||
|  | 				Scheme: "https", | ||||||
|  | 				Host:   "keycloak-oidc.com", | ||||||
|  | 				Path:   "/api/v3/user"}, | ||||||
|  | 			Scope: "openid email profile"}) | ||||||
|  | 
 | ||||||
|  | 	if serverURL != nil { | ||||||
|  | 		p.RedeemURL.Scheme = serverURL.Scheme | ||||||
|  | 		p.RedeemURL.Host = serverURL.Host | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	keyset := DummyKeySet{} | ||||||
|  | 	p.Verifier = oidc.NewVerifier("", keyset, &oidc.Config{ | ||||||
|  | 		ClientID:          "client", | ||||||
|  | 		SkipIssuerCheck:   true, | ||||||
|  | 		SkipClientIDCheck: true, | ||||||
|  | 		SkipExpiryCheck:   true, | ||||||
|  | 	}) | ||||||
|  | 	p.EmailClaim = "email" | ||||||
|  | 	p.GroupsClaim = "groups" | ||||||
|  | 	return p | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var _ = Describe("Keycloak OIDC Provider Tests", func() { | ||||||
|  | 	Context("New Provider Init", func() { | ||||||
|  | 		It("creates new keycloak oidc provider with expected defaults", func() { | ||||||
|  | 			p := newKeycloakOIDCProvider(nil) | ||||||
|  | 			providerData := p.Data() | ||||||
|  | 			Expect(providerData.ProviderName).To(Equal(keycloakOIDCProviderName)) | ||||||
|  | 			Expect(providerData.LoginURL.String()).To(Equal("https://keycloak-oidc.com/oauth/auth")) | ||||||
|  | 			Expect(providerData.RedeemURL.String()).To(Equal("https://keycloak-oidc.com/oauth/token")) | ||||||
|  | 			Expect(providerData.ProfileURL.String()).To(Equal("https://keycloak-oidc.com/api/v3/user")) | ||||||
|  | 			Expect(providerData.ValidateURL.String()).To(Equal("https://keycloak-oidc.com/api/v3/user")) | ||||||
|  | 			Expect(providerData.Scope).To(Equal("openid email profile")) | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	Context("Allowed Roles", func() { | ||||||
|  | 		It("should prefix allowed roles and add them to groups", func() { | ||||||
|  | 			p := newKeycloakOIDCProvider(nil) | ||||||
|  | 			p.AddAllowedRoles([]string{"admin", "editor"}) | ||||||
|  | 			Expect(p.AllowedGroups).To(HaveKey("role:admin")) | ||||||
|  | 			Expect(p.AllowedGroups).To(HaveKey("role:editor")) | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	Context("Enrich Session", func() { | ||||||
|  | 		It("should not fail when groups are not assigned", func() { | ||||||
|  | 			server, provider := newTestKeycloakOIDCSetup() | ||||||
|  | 			url, err := url.Parse(server.URL) | ||||||
|  | 			Expect(err).To(BeNil()) | ||||||
|  | 			defer server.Close() | ||||||
|  | 
 | ||||||
|  | 			provider.ProfileURL = url | ||||||
|  | 
 | ||||||
|  | 			existingSession := &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "a@b.com", | ||||||
|  | 				Groups:       nil, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  getAccessToken(), | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			} | ||||||
|  | 			expectedSession := &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "a@b.com", | ||||||
|  | 				Groups:       []string{"role:write", "role:default:read"}, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  getAccessToken(), | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			err = provider.EnrichSession(context.Background(), existingSession) | ||||||
|  | 			Expect(err).To(BeNil()) | ||||||
|  | 			Expect(existingSession).To(Equal(expectedSession)) | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		It("should add roles to existing groups", func() { | ||||||
|  | 			server, provider := newTestKeycloakOIDCSetup() | ||||||
|  | 			url, err := url.Parse(server.URL) | ||||||
|  | 			Expect(err).To(BeNil()) | ||||||
|  | 			defer server.Close() | ||||||
|  | 
 | ||||||
|  | 			provider.ProfileURL = url | ||||||
|  | 
 | ||||||
|  | 			existingSession := &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "a@b.com", | ||||||
|  | 				Groups:       []string{"existing", "group"}, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  getAccessToken(), | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			} | ||||||
|  | 			expectedSession := &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "a@b.com", | ||||||
|  | 				Groups:       []string{"existing", "group", "role:write", "role:default:read"}, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  getAccessToken(), | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			err = provider.EnrichSession(context.Background(), existingSession) | ||||||
|  | 			Expect(err).To(BeNil()) | ||||||
|  | 			Expect(existingSession).To(Equal(expectedSession)) | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	Context("Refresh Session", func() { | ||||||
|  | 		It("should refresh session and extract roles again", func() { | ||||||
|  | 			server, provider := newTestKeycloakOIDCSetup() | ||||||
|  | 			url, err := url.Parse(server.URL) | ||||||
|  | 			Expect(err).To(BeNil()) | ||||||
|  | 			defer server.Close() | ||||||
|  | 
 | ||||||
|  | 			provider.ProfileURL = url | ||||||
|  | 
 | ||||||
|  | 			existingSession := &sessions.SessionState{ | ||||||
|  | 				User:         "already", | ||||||
|  | 				Email:        "a@b.com", | ||||||
|  | 				Groups:       nil, | ||||||
|  | 				IDToken:      idToken, | ||||||
|  | 				AccessToken:  getAccessToken(), | ||||||
|  | 				RefreshToken: refreshToken, | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			refreshed, err := provider.RefreshSession(context.Background(), existingSession) | ||||||
|  | 			Expect(err).To(BeNil()) | ||||||
|  | 			Expect(refreshed).To(BeTrue()) | ||||||
|  | 			Expect(existingSession.ExpiresOn).ToNot(BeNil()) | ||||||
|  | 			Expect(existingSession.CreatedAt).ToNot(BeNil()) | ||||||
|  | 			Expect(existingSession.Groups).To(BeEquivalentTo([]string{"role:write", "role:default:read"})) | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | }) | ||||||
|  | @ -31,6 +31,8 @@ func New(provider string, p *ProviderData) Provider { | ||||||
| 		return NewGitHubProvider(p) | 		return NewGitHubProvider(p) | ||||||
| 	case "keycloak": | 	case "keycloak": | ||||||
| 		return NewKeycloakProvider(p) | 		return NewKeycloakProvider(p) | ||||||
|  | 	case "keycloak-oidc": | ||||||
|  | 		return NewKeycloakOIDCProvider(p) | ||||||
| 	case "azure": | 	case "azure": | ||||||
| 		return NewAzureProvider(p) | 		return NewAzureProvider(p) | ||||||
| 	case "adfs": | 	case "adfs": | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue