Repository: oauth2-proxy/oauth2-proxy
Pull request #1271 (2021-07-15 07:34:09)

Fix `go get` instructions
## Description

Updates the version in the README, and adds `/v7` to the `go build` command

## Motivation and Context

It looks like the old `go get` command was installing an old version. I suspect this 3.x one: https://pkg.go.dev/github.com/oauth2-proxy/oauth2-proxy

## How Has This Been Tested?

I noticed this because `--insecure-oidc-skip-nonce` wasn't working, but afterwards you can tell by the completely new design. 🙃

## Checklist:

- [ ] My change requires a change to the documentation or CHANGELOG.
- [ ] I have updated the documentation/CHANGELOG accordingly.
- [ ] I have created a feature (non-master) branch for my PR.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2d5f904c..6e717672 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,11 +8,14 @@
   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
 
 ## Changes since v7.1.3
 
+- [#1276](https://github.com/oauth2-proxy/oauth2-proxy/pull/1276) Update crypto and switched to new github.com/golang-jwt/jwt (@JVecsei)
 - [#1264](https://github.com/oauth2-proxy/oauth2-proxy/pull/1264) Update go-oidc to v3 (@NickMeves)
 - [#1233](https://github.com/oauth2-proxy/oauth2-proxy/pull/1233) Extend email-domain validation with sub-domain capability (@morarucostel)
 - [#1060](https://github.com/oauth2-proxy/oauth2-proxy/pull/1060) Implement RewriteTarget to allow requests to be rewritten before proxying to upstream servers (@JoelSpeed)
@@ -26,8 +29,9 @@
 - [#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)
-
+- [#1317](https://github.com/oauth2-proxy/oauth2-proxy/pull/1317) Fix incorrect `</form>` tag on the sing_in page when *not* using a custom template (@jord1e)
 
 # V7.1.3
 
diff --git a/README.md b/README.md
index 6995f45a..3b1e92c3 100644
--- a/README.md
+++ b/README.md
@@ -23,9 +23,9 @@ Going forward, all images shall be available at `quay.io/oauth2-proxy/oauth2-pro
 
 1.  Choose how to deploy:
 
-    a. Download [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.1.0`)
+    a. Download [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.1.3`)
 
-    b. Build with `$ go get github.com/oauth2-proxy/oauth2-proxy` which will put the binary in `$GOROOT/bin`
+    b. Build with `$ go get github.com/oauth2-proxy/oauth2-proxy/v7` which will put the binary in `$GOROOT/bin`
 
     c. Using the prebuilt docker image [quay.io/oauth2-proxy/oauth2-proxy](https://quay.io/oauth2-proxy/oauth2-proxy) (AMD64, ARMv6 and ARM64 tags available)
 
diff --git a/docs/docs/configuration/alpha_config.md b/docs/docs/configuration/alpha_config.md
index 7f44e343..7381e1c0 100644
--- a/docs/docs/configuration/alpha_config.md
+++ b/docs/docs/configuration/alpha_config.md
@@ -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
 
diff --git a/docs/docs/configuration/auth.md b/docs/docs/configuration/auth.md
index 0673c295..576c5819 100644
--- a/docs/docs/configuration/auth.md
+++ b/docs/docs/configuration/auth.md
@@ -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)).
diff --git a/docs/docs/configuration/overview.md b/docs/docs/configuration/overview.md
index ebf7c05e..7d92d2ec 100644
--- a/docs/docs/configuration/overview.md
+++ b/docs/docs/configuration/overview.md
@@ -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`)&nbsp;\[[2](#footnote2)\] | |
diff --git a/go.mod b/go.mod
index 21b350bd..15c3e7d9 100644
--- a/go.mod
+++ b/go.mod
@@ -10,11 +10,11 @@ require (
 	github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
 	github.com/bsm/redislock v0.7.0
 	github.com/coreos/go-oidc/v3 v3.0.0
-	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/frankban/quicktest v1.10.0 // indirect
 	github.com/fsnotify/fsnotify v1.4.9
 	github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
 	github.com/go-redis/redis/v8 v8.2.3
+	github.com/golang-jwt/jwt v3.2.1+incompatible
 	github.com/google/uuid v1.2.0
 	github.com/gorilla/mux v1.8.0
 	github.com/justinas/alice v1.2.0
@@ -30,8 +30,8 @@ require (
 	github.com/stretchr/testify v1.6.1
 	github.com/vmihailenco/msgpack/v4 v4.3.11
 	github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997
-	golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
-	golang.org/x/net v0.0.0-20200822124328-c89045814202
+	golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
+	golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
 	golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
 	google.golang.org/api v0.20.0
diff --git a/go.sum b/go.sum
index fa5417b3..fe496af7 100644
--- a/go.sum
+++ b/go.sum
@@ -81,7 +81,6 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
@@ -135,6 +134,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
+github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -457,8 +458,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
-golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw=
@@ -501,8 +502,8 @@ golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
-golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -544,8 +545,11 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
diff --git a/pkg/apis/options/legacy_options.go b/pkg/apis/options/legacy_options.go
index 17cbcbc3..cf67dedd 100644
--- a/pkg/apis/options/legacy_options.go
+++ b/pkg/apis/options/legacy_options.go
@@ -508,6 +508,7 @@ type LegacyProvider struct {
 	ApprovalPrompt                     string   `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0
 	UserIDClaim                        string   `flag:"user-id-claim" cfg:"user_id_claim"`
 	AllowedGroups                      []string `flag:"allowed-group" cfg:"allowed_groups"`
+	AllowedRoles                       []string `flag:"allowed-role" cfg:"allowed_roles"`
 
 	AcrValues  string `flag:"acr-values" cfg:"acr_values"`
 	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.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
 }
@@ -656,6 +658,11 @@ func (l *LegacyProvider) convert() (Providers, error) {
 			Token: l.GitHubToken,
 			Users: l.GitHubUsers,
 		}
+	case "keycloak-oidc":
+		provider.KeycloakConfig = KeycloakOptions{
+			Groups: l.KeycloakGroups,
+			Roles:  l.AllowedRoles,
+		}
 	case "keycloak":
 		provider.KeycloakConfig = KeycloakOptions{
 			Groups: l.KeycloakGroups,
diff --git a/pkg/apis/options/providers.go b/pkg/apis/options/providers.go
index 12248b3f..172479fa 100644
--- a/pkg/apis/options/providers.go
+++ b/pkg/apis/options/providers.go
@@ -78,6 +78,9 @@ type Provider struct {
 type KeycloakOptions struct {
 	// Group enables to restrict login to members of indicated group
 	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 {
diff --git a/pkg/app/pagewriter/sign_in.html b/pkg/app/pagewriter/sign_in.html
index fc7ab3ff..5d2ff051 100644
--- a/pkg/app/pagewriter/sign_in.html
+++ b/pkg/app/pagewriter/sign_in.html
@@ -60,8 +60,8 @@
           </div>
         </div>
         <button class="button is-primary">Sign in</button>
-        {{ end }}
-    </form>
+      </form>
+      {{ end }}
     </div>
   </section>
 
diff --git a/pkg/validation/options.go b/pkg/validation/options.go
index 255f8899..37c2aa24 100644
--- a/pkg/validation/options.go
+++ b/pkg/validation/options.go
@@ -11,7 +11,7 @@ import (
 	"strings"
 
 	"github.com/coreos/go-oidc/v3/oidc"
-	"github.com/dgrijalva/jwt-go"
+	"github.com/golang-jwt/jwt"
 	"github.com/mbland/hmacauth"
 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
@@ -247,6 +247,11 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
 		if len(o.Providers[0].KeycloakConfig.Groups) > 0 {
 			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:
 		if o.Providers[0].GoogleConfig.ServiceAccountJSON != "" {
 			file, err := os.Open(o.Providers[0].GoogleConfig.ServiceAccountJSON)
diff --git a/providers/keycloak_oidc.go b/providers/keycloak_oidc.go
new file mode 100644
index 00000000..cb1971db
--- /dev/null
+++ b/providers/keycloak_oidc.go
@@ -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)
+}
diff --git a/providers/logingov.go b/providers/logingov.go
index 43f361f3..80b5e7ad 100644
--- a/providers/logingov.go
+++ b/providers/logingov.go
@@ -10,7 +10,7 @@ import (
 	"net/url"
 	"time"
 
-	"github.com/dgrijalva/jwt-go"
+	"github.com/golang-jwt/jwt"
 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
 	"gopkg.in/square/go-jose.v2"
diff --git a/providers/providers.go b/providers/providers.go
index d21409c2..a192f220 100644
--- a/providers/providers.go
+++ b/providers/providers.go
@@ -31,6 +31,8 @@ func New(provider string, p *ProviderData) Provider {
 		return NewGitHubProvider(p)
 	case "keycloak":
 		return NewKeycloakProvider(p)
+	case "keycloak-oidc":
+		return NewKeycloakOIDCProvider(p)
 	case "azure":
 		return NewAzureProvider(p)
 	case "adfs":


Test patch:
diff --git a/pkg/middleware/jwt_session_test.go b/pkg/middleware/jwt_session_test.go
index fad90d91..d9a586fe 100644
--- a/pkg/middleware/jwt_session_test.go
+++ b/pkg/middleware/jwt_session_test.go
@@ -14,7 +14,7 @@ import (
 	"time"
 
 	"github.com/coreos/go-oidc/v3/oidc"
-	"github.com/dgrijalva/jwt-go"
+	"github.com/golang-jwt/jwt"
 	middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
 	sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
 	. "github.com/onsi/ginkgo"
diff --git a/providers/keycloak_oidc_test.go b/providers/keycloak_oidc_test.go
new file mode 100644
index 00000000..686295ea
--- /dev/null
+++ b/providers/keycloak_oidc_test.go
@@ -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"}))
+		})
+	})
+})
diff --git a/providers/logingov_test.go b/providers/logingov_test.go
index 2287e643..3ed0f520 100644
--- a/providers/logingov_test.go
+++ b/providers/logingov_test.go
@@ -12,7 +12,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/dgrijalva/jwt-go"
+	"github.com/golang-jwt/jwt"
 	. "github.com/onsi/gomega"
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/square/go-jose.v2"
diff --git a/providers/provider_data_test.go b/providers/provider_data_test.go
index 8d26476c..09a19ded 100644
--- a/providers/provider_data_test.go
+++ b/providers/provider_data_test.go
@@ -13,7 +13,7 @@ import (
 	"time"
 
 	"github.com/coreos/go-oidc/v3/oidc"
-	"github.com/dgrijalva/jwt-go"
+	"github.com/golang-jwt/jwt"
 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
 	. "github.com/onsi/gomega"