Skip to content

Commit 26cd9b7

Browse files
authored
Add possibility to specify claims from id token for upstream headers (#676)
1 parent 828147b commit 26cd9b7

File tree

13 files changed

+165
-13
lines changed

13 files changed

+165
-13
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ jobs:
2828
uses: AbsaOSS/k3d-action@v2.4.0
2929
with:
3030
cluster-name: "testcluster"
31+
with:
32+
k3d-version: v5.8.3
3133
args: >-
3234
-p "8443:443@loadbalancer"
3335
- name: "Deploy keycloak"

docs/content/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,7 @@ option is set to `true`.
751751

752752
## Custom claim headers
753753

754-
You can inject additional claims from the access token into the
754+
You can inject additional claims from the access token and from version 4.6.0 also from ID token into the
755755
upstream headers with the `--add-claims` option. For example, a
756756
token from a Keycloak provider might include the following
757757
claims:

docs/content/configuration/_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ weight: 2
157157
| \--enable-logout-auth | enable authentication on logout handler | true | PROXY_ENABLE_LOGOUT_AUTH
158158
| \--enable-request-upstream-compression | enables asking upstream for compression, by adding Accept-Encoding: gzip header and decompressing response from upstream | true | PROXY_ENABLE_REQUEST_UPSTREAM_COMPRESSION
159159
| \--enable-accept-encoding-header | pass Accept-Encoding header from client to upstream | false | PROXY_ENABLE_ACCEPT_ENCODING_HEADER
160+
| \--enable-id-token-claims | extract claims also from ID token, you can then use --add-claims option to specify claims from ID token | false | PROXY_ENABLE_ID_TOKEN_CLAIMS
160161
| \--enable-loa | enable level of authentication | false |
161162
| \--enable-store-ha | enable RedisCluster HA client | false | PROXY_ENABLE_STORE_HA
162163
| \--disable-all-logging | disables all logging to stdout and stderr | false | PROXY_DISABLE_ALL_LOGGING

e2e/e2e_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,9 @@ var _ = Describe("Code Flow login/logout", func() {
591591
"--post-login-redirect-path=" + postLoginRedirectPath,
592592
"--enable-register-handler=true",
593593
"--enable-encrypted-token=false",
594+
"--enable-id-token-claims=true",
595+
"--enable-id-token-cookie=true",
596+
"--add-claims=email_verified",
594597
"--enable-pkce=false",
595598
"--tls-cert=" + tlsCertificate,
596599
"--tls-private-key=" + tlsPrivateKey,
@@ -650,8 +653,9 @@ var _ = Describe("Code Flow login/logout", func() {
650653
Expect(err).NotTo(HaveOccurred())
651654
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
652655
body = resp.Body()
653-
Expect(strings.Contains(string(body), anyURI)).To(BeTrue())
654656
Expect(resp.StatusCode()).To(Equal(http.StatusOK))
657+
Expect(strings.Contains(string(body), anyURI)).To(BeTrue())
658+
Expect(body).To(ContainSubstring("X-Auth-Email-Verified"))
655659

656660
By("log out")
657661
resp, err = rClient.R().Get(proxyAddress + logoutURI)

e2e/k8s/manifest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1011,7 +1011,7 @@ data:
10111011
"userinfo.token.claim": "true",
10121012
"user.attribute": "emailVerified",
10131013
"id.token.claim": "true",
1014-
"access.token.claim": "true",
1014+
"access.token.claim": "false",
10151015
"claim.name": "email_verified",
10161016
"jsonType.label": "boolean"
10171017
}

pkg/apperrors/apperrors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ var (
6161
ErrDecryptTokenSignature = errors.New("unable to decrypt token signature")
6262
ErrDecompressToken = errors.New("unable to decompress token")
6363
ErrDecryptAndDecompressToken = errors.New("unable to decrypt and decompress token")
64+
ErrExtractIDTokenClaims = errors.New("problem extracting idToken claims")
6465

6566
ErrDelTokFromStore = errors.New("failed to remove old token")
6667
ErrSaveTokToStore = errors.New("failed to store refresh token")
@@ -252,4 +253,5 @@ var (
252253
ErrNegativeisPatRetryCount = errors.New("pat retry count must be greater than zero")
253254
ErrEnableTokenCompression = errors.New("cannot enable token compression " +
254255
"with optional token encryption")
256+
ErrEnableIDTokenClaims = errors.New("enable id token claims requires also enable-id-token-cookie option")
255257
)

pkg/keycloak/config/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ type Config struct {
200200
EnableLogoutAuth bool `env:"ENABLE_LOGOUT_AUTH" json:"enable-logout-auth" usage:"enable authentication on logout handler" yaml:"enable-logout-auth"`
201201
EnableRequestUpstreamCompression bool `env:"ENABLE_REQUEST_UPSTREAM_COMPRESSION" json:"enable-request-upstream-compression" usage:"enables asking upstream for compression, by adding Accept-Encoding: gzip header and decompressing response from upstream" yaml:"enable-request-upstream-compression"`
202202
EnableAcceptEncodingHeader bool `env:"ENABLE_ACCEPT_ENCODING_HEADER" json:"enable-accept-encoding-header" usage:"pass Accept-Encoding header from client to upstream" yaml:"enable-accept-encoding-header"`
203+
EnableIDTokenClaims bool `env:"ENABLE_ID_TOKEN_CLAIMS" json:"enable-id-token-claims" usage:"extract claims also from id token, you can then use --add-claims option to specify claims from id token" yaml:"enable-id-token-claims"`
203204
IsDiscoverURILegacy bool
204205
}
205206

@@ -658,6 +659,7 @@ func (r *Config) isReverseProxySettingsValid() error {
658659
r.isEnableXForwardedHeadersValid,
659660
r.isEnableOptionalEncryptionValid,
660661
r.isEnableLogoutAuthValid,
662+
r.isEnableIDTokenClaimsValid,
661663
}
662664

663665
for _, validationFunc := range validationRegistry {
@@ -1168,3 +1170,11 @@ func (r *Config) isEnableCompressTokenValid() error {
11681170

11691171
return nil
11701172
}
1173+
1174+
func (r *Config) isEnableIDTokenClaimsValid() error {
1175+
if r.EnableIDTokenClaims && !r.EnableIDTokenCookie {
1176+
return apperrors.ErrEnableIDTokenClaims
1177+
}
1178+
1179+
return nil
1180+
}

pkg/keycloak/config/config_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3380,3 +3380,52 @@ func TestIsEnableCompressTokenValid(t *testing.T) {
33803380
)
33813381
}
33823382
}
3383+
3384+
func TestIsEnableIDTokenClaimsValid(t *testing.T) {
3385+
testCases := []struct {
3386+
Name string
3387+
Config *Config
3388+
Valid bool
3389+
}{
3390+
{
3391+
Name: "ValidEnableIDTokenClaims",
3392+
Config: &Config{
3393+
EnableIDTokenClaims: true,
3394+
EnableIDTokenCookie: true,
3395+
},
3396+
Valid: true,
3397+
},
3398+
{
3399+
Name: "ValidDisabledEnableIDTokenClaims",
3400+
Config: &Config{
3401+
EnableIDTokenClaims: false,
3402+
EnableIDTokenCookie: false,
3403+
},
3404+
Valid: true,
3405+
},
3406+
{
3407+
Name: "InValidEnableIDTokenClaimsValid",
3408+
Config: &Config{
3409+
EnableIDTokenClaims: true,
3410+
EnableIDTokenCookie: false,
3411+
},
3412+
Valid: false,
3413+
},
3414+
}
3415+
3416+
for _, testCase := range testCases {
3417+
t.Run(
3418+
testCase.Name,
3419+
func(t *testing.T) {
3420+
err := testCase.Config.isEnableIDTokenClaimsValid()
3421+
if err != nil && testCase.Valid {
3422+
t.Fatalf("Expected test not to fail")
3423+
}
3424+
3425+
if err == nil && !testCase.Valid {
3426+
t.Fatalf("Expected test to fail")
3427+
}
3428+
},
3429+
)
3430+
}
3431+
}

pkg/keycloak/proxy/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,7 @@ func (r *OauthProxy) CreateReverseProxy() error {
563563
r.Config.AccessTokenDuration,
564564
r.Config.EnableOptionalEncryption,
565565
r.Config.EnableCompressToken,
566+
r.Config.EnableIDTokenClaims,
566567
compressTokenPool,
567568
)
568569

@@ -856,6 +857,7 @@ func (r *OauthProxy) CreateReverseProxy() error {
856857
r.Config.EnableAuthorizationHeader,
857858
r.Config.EnableAuthorizationCookies,
858859
r.Config.EnableHeaderEncoding,
860+
r.Config.EnableIDTokenClaims,
859861
)
860862

861863
middlewares := []func(http.Handler) http.Handler{

pkg/proxy/middleware/base.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ func IdentityHeadersMiddleware(
226226
enableAuthzHeader bool,
227227
enableAuthzCookies bool,
228228
enableHeaderEncoding bool,
229+
enableIDTokenClaims bool,
229230
) func(http.Handler) http.Handler {
230231
customClaims := make(map[string]string)
231232

@@ -305,6 +306,17 @@ func IdentityHeadersMiddleware(
305306

306307
headers.Set(header, val)
307308
}
309+
310+
if enableIDTokenClaims {
311+
if claim, found := user.IDTokenClaims[claim]; found {
312+
val := fmt.Sprintf("%v", claim)
313+
if enableHeaderEncoding {
314+
val = mime.BEncoding.Encode(encoding, val)
315+
}
316+
317+
headers.Set(header, val)
318+
}
319+
}
308320
}
309321
}
310322

0 commit comments

Comments
 (0)