1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-06-27 00:51:33 +02:00

update keycloak oidc provider and add unit tests

This commit is contained in:
Peter Braun
2021-05-05 16:18:02 +02:00
parent ab54de38cc
commit e6223383e5
8 changed files with 221 additions and 27 deletions

View File

@ -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)

View File

@ -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

View File

@ -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)).

View File

@ -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`)&nbsp;\[[2](#footnote2)\] | | | `--whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (e.g. `.example.com`)&nbsp;\[[2](#footnote2)\] | |

View File

@ -658,11 +658,15 @@ func (l *LegacyProvider) convert() (Providers, error) {
Token: l.GitHubToken, Token: l.GitHubToken,
Users: l.GitHubUsers, Users: l.GitHubUsers,
} }
case "keycloak": case "keycloak-oidc":
provider.KeycloakConfig = KeycloakOptions{ provider.KeycloakConfig = KeycloakOptions{
Groups: l.KeycloakGroups, Groups: l.KeycloakGroups,
Roles: l.AllowedRoles, Roles: l.AllowedRoles,
} }
case "keycloak":
provider.KeycloakConfig = KeycloakOptions{
Groups: l.KeycloakGroups,
}
case "gitlab": case "gitlab":
provider.GitLabConfig = GitLabOptions{ provider.GitLabConfig = GitLabOptions{
Group: l.GitLabGroup, Group: l.GitLabGroup,

View File

@ -78,6 +78,8 @@ 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"` Roles []string `json:"roles,omitempty"`
} }

View File

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
) )
const keycloakOIDCProviderName = "Keycloak OIDC" const keycloakOIDCProviderName = "Keycloak OIDC"
@ -31,6 +30,9 @@ var _ Provider = (*KeycloakOIDCProvider)(nil)
// Assumes `SetAllowedGroups` is already called on groups and appends to that // Assumes `SetAllowedGroups` is already called on groups and appends to that
// with `role:` prefixed roles. // with `role:` prefixed roles.
func (p *KeycloakOIDCProvider) AddAllowedRoles(roles []string) { func (p *KeycloakOIDCProvider) AddAllowedRoles(roles []string) {
if p.AllowedGroups == nil {
p.AllowedGroups = make(map[string]struct{})
}
for _, role := range roles { for _, role := range roles {
p.AllowedGroups[formatRole(role)] = struct{}{} p.AllowedGroups[formatRole(role)] = struct{}{}
} }
@ -41,11 +43,23 @@ func (p *KeycloakOIDCProvider) AddAllowedRoles(roles []string) {
func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error { func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
err := p.OIDCProvider.EnrichSession(ctx, s) err := p.OIDCProvider.EnrichSession(ctx, s)
if err != nil { if err != nil {
return err return fmt.Errorf("could not enrich oidc session: %v", err)
} }
return p.extractRoles(ctx, s) 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 { func (p *KeycloakOIDCProvider) extractRoles(ctx context.Context, s *sessions.SessionState) error {
claims, err := p.getAccessClaims(ctx, s) claims, err := p.getAccessClaims(ctx, s)
if err != nil { if err != nil {
@ -109,7 +123,6 @@ func getClientRoles(claims *accessClaims) []string {
for clientName, access := range claims.ResourceAccess { for clientName, access := range claims.ResourceAccess {
accessMap, ok := access.(map[string]interface{}) accessMap, ok := access.(map[string]interface{})
if !ok { if !ok {
logger.Errorf("Unable to parse client roles from claims for client: %v", clientName)
continue continue
} }

View File

@ -1,15 +1,43 @@
package providers package providers
import ( import (
"context"
"encoding/base64"
"fmt"
"net/http/httptest"
"net/url" "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/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
var _ = Describe("Keycloak OIDC Provider Tests", func() { const (
Context("New Provider Init", func() { accessTokenHeader = "ewogICJhbGciOiAiUlMyNTYiLAogICJ0eXAiOiAiSldUIgp9"
It("uses the passed ProviderData", func() { 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( p := NewKeycloakOIDCProvider(
&ProviderData{ &ProviderData{
LoginURL: &url.URL{ LoginURL: &url.URL{
@ -29,8 +57,29 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
Host: "keycloak-oidc.com", Host: "keycloak-oidc.com",
Path: "/api/v3/user"}, Path: "/api/v3/user"},
Scope: "openid email profile"}) Scope: "openid email profile"})
providerData := p.Data()
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.ProviderName).To(Equal(keycloakOIDCProviderName))
Expect(providerData.LoginURL.String()).To(Equal("https://keycloak-oidc.com/oauth/auth")) 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.RedeemURL.String()).To(Equal("https://keycloak-oidc.com/oauth/token"))
@ -39,4 +88,102 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
Expect(providerData.Scope).To(Equal("openid email profile")) 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"}))
})
})
}) })