mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-02-15 14:03:45 +02:00
update keycloak oidc provider and add unit tests
This commit is contained in:
parent
ab54de38cc
commit
e6223383e5
@ -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
|
||||
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.
|
||||
|
||||
- [#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
|
||||
|
||||
@ -27,6 +29,7 @@
|
||||
- [#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)
|
||||
- [#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)
|
||||
|
||||
|
||||
|
@ -250,6 +250,7 @@ make up the header value
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `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
|
||||
|
||||
|
@ -146,12 +146,15 @@ If you are using GitHub enterprise, make sure you set the following to the appro
|
||||
|
||||
### 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
|
||||
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:
|
||||
|
||||
```
|
||||
--provider=keycloak
|
||||
--client-id=<client you have created>
|
||||
--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"
|
||||
--keycloak-group=<first_allowed_user_group>
|
||||
--keycloak-group=<second_allowed_user_group>
|
||||
```
|
||||
|
||||
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.
|
||||
@ -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
|
||||
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
|
||||
|
||||
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 | |
|
||||
| `--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-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 | |
|
||||
| `--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)\] | |
|
||||
|
@ -658,11 +658,15 @@ func (l *LegacyProvider) convert() (Providers, error) {
|
||||
Token: l.GitHubToken,
|
||||
Users: l.GitHubUsers,
|
||||
}
|
||||
case "keycloak":
|
||||
case "keycloak-oidc":
|
||||
provider.KeycloakConfig = KeycloakOptions{
|
||||
Groups: l.KeycloakGroups,
|
||||
Roles: l.AllowedRoles,
|
||||
}
|
||||
case "keycloak":
|
||||
provider.KeycloakConfig = KeycloakOptions{
|
||||
Groups: l.KeycloakGroups,
|
||||
}
|
||||
case "gitlab":
|
||||
provider.GitLabConfig = GitLabOptions{
|
||||
Group: l.GitLabGroup,
|
||||
|
@ -78,7 +78,9 @@ type Provider struct {
|
||||
type KeycloakOptions struct {
|
||||
// Group enables to restrict login to members of indicated group
|
||||
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 {
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
|
||||
)
|
||||
|
||||
const keycloakOIDCProviderName = "Keycloak OIDC"
|
||||
@ -31,6 +30,9 @@ var _ Provider = (*KeycloakOIDCProvider)(nil)
|
||||
// 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{}{}
|
||||
}
|
||||
@ -41,11 +43,23 @@ func (p *KeycloakOIDCProvider) AddAllowedRoles(roles []string) {
|
||||
func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
|
||||
err := p.OIDCProvider.EnrichSession(ctx, s)
|
||||
if err != nil {
|
||||
return err
|
||||
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 {
|
||||
@ -109,7 +123,6 @@ func getClientRoles(claims *accessClaims) []string {
|
||||
for clientName, access := range claims.ResourceAccess {
|
||||
accessMap, ok := access.(map[string]interface{})
|
||||
if !ok {
|
||||
logger.Errorf("Unable to parse client roles from claims for client: %v", clientName)
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -1,36 +1,85 @@
|
||||
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("uses the passed ProviderData", func() {
|
||||
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"})
|
||||
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"))
|
||||
@ -39,4 +88,102 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
|
||||
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"}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user