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

@ -8,6 +8,8 @@
deserialization from v6.0.0 (only) has been removed to improve performance. If you are on v6.0.0, either upgrade deserialization from v6.0.0 (only) has been removed to improve performance. If you are on v6.0.0, either upgrade
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
@ -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,7 +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"`
Roles []string `json:"roles,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 {

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,36 +1,85 @@
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"
) )
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() { var _ = Describe("Keycloak OIDC Provider Tests", func() {
Context("New Provider Init", func() { Context("New Provider Init", func() {
It("uses the passed ProviderData", func() { It("creates new keycloak oidc provider with expected defaults", func() {
p := NewKeycloakOIDCProvider( p := newKeycloakOIDCProvider(nil)
&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"})
providerData := p.Data() 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"}))
})
})
}) })