Skip to content
Snippets Groups Projects
Commit 4603ad91 authored by Frederic BIDON's avatar Frederic BIDON Committed by Bruno Oliveira da Silva
Browse files

[KEYCLOAK-9074] Optionally enforce token encryption in cookie


* add config option to distinguish when to encrypt token: in header or/and in cookie
* fixes #9704

Signed-off-by: default avatarFrederic BIDON <frederic@oneconcern.com>
parent 63052004
No related branches found
No related tags found
No related merge requests found
......@@ -173,7 +173,7 @@ func (r *Config) isValid() error {
return errors.New("the security filter must be switch on for this feature: hostnames")
}
}
if r.EnableEncryptedToken && r.EncryptionKey == "" {
if (r.EnableEncryptedToken || r.ForceEncryptedCookie) && r.EncryptionKey == "" {
return errors.New("you have not specified an encryption key for encoding the access token")
}
if r.EnableRefreshTokens && r.EncryptionKey == "" {
......
......@@ -211,6 +211,8 @@ type Config struct {
EnableDefaultDeny bool `json:"enable-default-deny" yaml:"enable-default-deny" usage:"enables a default denial on all requests, you have to explicitly say what is permitted (recommended)"`
// EnableEncryptedToken indicates the access token should be encoded
EnableEncryptedToken bool `json:"enable-encrypted-token" yaml:"enable-encrypted-token" usage:"enable encryption for the access tokens"`
// ForceEncryptedCookie indicates that the access token in the cookie should be encoded, regardless what EnableEncryptedToken says. This way, gatekeeper may receive tokens in header in the clear, whereas tokens in cookies remain encrypted
ForceEncryptedCookie bool `json:"force-encrypted-cookie" yaml:"force-encrypted-cookie" usage:"force encryption for the access tokens in cookies"`
// EnableLogging indicates if we should log all the requests
EnableLogging bool `json:"enable-logging" yaml:"enable-logging" usage:"enable http logging of the requests"`
// EnableJSONLogging is the logging format
......@@ -227,7 +229,7 @@ type Config struct {
EnableLoginHandler bool `json:"enable-login-handler" yaml:"enable-login-handler" usage:"enables the handling of the refresh tokens" env:"ENABLE_LOGIN_HANDLER"`
// EnableTokenHeader adds the JWT token to the upstream authentication headers
EnableTokenHeader bool `json:"enable-token-header" yaml:"enable-token-header" usage:"enables the token authentication header X-Auth-Token to upstream"`
// EnableAuthorizationHeader indicates we should pass the authorization header
// EnableAuthorizationHeader indicates we should pass the authorization header to the upstream endpoint
EnableAuthorizationHeader bool `json:"enable-authorization-header" yaml:"enable-authorization-header" usage:"adds the authorization header to the proxy request" env:"ENABLE_AUTHORIZATION_HEADER"`
// EnableAuthorizationCookies indicates we should pass the authorization cookies to the upstream endpoint
EnableAuthorizationCookies bool `json:"enable-authorization-cookies" yaml:"enable-authorization-cookies" usage:"adds the authorization cookies to the uptream proxy request" env:"ENABLE_AUTHORIZATION_COOKIES"`
......
......@@ -142,8 +142,8 @@ func (r *oauthProxy) oauthCallbackHandler(w http.ResponseWriter, req *http.Reque
return
}
// Flow: once we exchange the authorization code we parse the ID Token; we then check for a access token,
// if a access token is present and we can decode it, we use that as the session token, otherwise we default
// Flow: once we exchange the authorization code we parse the ID Token; we then check for an access token,
// if an access token is present and we can decode it, we use that as the session token, otherwise we default
// to the ID Token.
token, identity, err := parseToken(resp.IDToken)
if err != nil {
......@@ -168,7 +168,7 @@ func (r *oauthProxy) oauthCallbackHandler(w http.ResponseWriter, req *http.Reque
accessToken := token.Encode()
// step: are we encrypting the access token?
if r.config.EnableEncryptedToken {
if r.config.EnableEncryptedToken || r.config.ForceEncryptedCookie {
if accessToken, err = encodeText(accessToken, r.config.EncryptionKey); err != nil {
r.log.Error("unable to encode the access token", zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
......@@ -181,10 +181,10 @@ func (r *oauthProxy) oauthCallbackHandler(w http.ResponseWriter, req *http.Reque
zap.String("expires", identity.ExpiresAt.Format(time.RFC3339)),
zap.String("duration", time.Until(identity.ExpiresAt).String()))
// @metric a token has beeb issued
// @metric a token has been issued
oauthTokensMetric.WithLabelValues("issued").Inc()
// step: does the response has a refresh token and we are NOT ignore refresh tokens?
// step: does the response have a refresh token and we do NOT ignore refresh tokens?
if r.config.EnableRefreshTokens && resp.RefreshToken != "" {
var encrypted string
encrypted, err = encodeText(resp.RefreshToken, r.config.EncryptionKey)
......
......@@ -194,7 +194,7 @@ func (r *oauthProxy) authenticationMiddleware(resource *Resource) func(http.Hand
zap.Duration("expires_in", time.Until(exp)))
accessToken := token.Encode()
if r.config.EnableEncryptedToken {
if r.config.EnableEncryptedToken || r.config.ForceEncryptedCookie {
if accessToken, err = encodeText(accessToken, r.config.EncryptionKey); err != nil {
r.log.Error("unable to encode the access token", zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
......
......@@ -34,6 +34,10 @@ import (
"gopkg.in/resty.v1"
)
const (
testEncryptionKey = "ZSeCYDUxIlhDrmPpa1Ldc7il384esSF2"
)
type fakeRequest struct {
BasicAuth bool
Cookies []*http.Cookie
......@@ -66,6 +70,9 @@ type fakeRequest struct {
ExpectedNoProxyHeaders []string
ExpectedProxy bool
ExpectedProxyHeaders map[string]string
// advanced test cases
ExpectedCookiesValidator map[string]func(string) bool
}
type fakeProxy struct {
......@@ -272,6 +279,15 @@ func (f *fakeProxy) RunTests(t *testing.T, requests []fakeRequest) {
assert.Equal(t, cookie.Value, v, "case %d, expected cookie value: %s, got: %s", i, v, cookie.Value)
}
}
for k, v := range c.ExpectedCookiesValidator {
cookie := findCookie(k, resp.Cookies())
if !assert.NotNil(t, cookie, "case %d, expected cookie %s not found", i, k) {
continue
}
if v != nil {
assert.True(t, v(cookie.Value), "case %d, invalid cookie value: %s", i, cookie.Value)
}
}
}
if c.OnResponse != nil {
c.OnResponse(i, request, resp)
......@@ -1072,12 +1088,78 @@ func TestCrossSiteHandler(t *testing.T) {
func TestCheckRefreshTokens(t *testing.T) {
cfg := newFakeKeycloakConfig()
cfg.EnableRefreshTokens = true
cfg.EncryptionKey = "ZSeCYDUxIlhDrmPpa1Ldc7il384esSF2"
cfg.EncryptionKey = testEncryptionKey
fn := func(no int, req *resty.Request, resp *resty.Response) {
if no == 0 {
<-time.After(1000 * time.Millisecond)
}
}
p := newFakeProxy(cfg)
p.idp.setTokenExpiration(1000 * time.Millisecond)
requests := []fakeRequest{
{
URI: fakeAuthAllURL,
HasLogin: true,
Redirects: true,
OnResponse: fn,
ExpectedProxy: true,
ExpectedCode: http.StatusOK,
},
{
URI: fakeAuthAllURL,
Redirects: false,
ExpectedProxy: true,
ExpectedCode: http.StatusOK,
ExpectedCookies: map[string]string{cfg.CookieAccessName: ""},
},
}
p.RunTests(t, requests)
}
func TestCheckEncryptedCookie(t *testing.T) {
cfg := newFakeKeycloakConfig()
cfg.EnableRefreshTokens = true
cfg.EnableEncryptedToken = true
cfg.Verbose = true
cfg.EnableLogging = true
cfg.EncryptionKey = testEncryptionKey
testEncryptedToken(t, cfg)
}
func TestCheckForcedEncryptedCookie(t *testing.T) {
cfg := newFakeKeycloakConfig()
cfg.EnableRefreshTokens = true
cfg.EnableEncryptedToken = false
cfg.ForceEncryptedCookie = true
cfg.Verbose = true
cfg.EnableLogging = true
cfg.EncryptionKey = testEncryptionKey
testEncryptedToken(t, cfg)
}
func testEncryptedToken(t *testing.T, cfg *Config) {
fn := func(no int, req *resty.Request, resp *resty.Response) {
if no == 0 {
<-time.After(1000 * time.Millisecond)
}
}
val := func(value string) bool {
// check the cookie value is an encrypted token
accessToken, err := decodeText(value, cfg.EncryptionKey)
if err != nil {
return false
}
jwt, err := jose.ParseJWT(accessToken)
if err != nil {
return false
}
claims, err := jwt.Claims()
if err != nil {
return false
}
return assert.Contains(t, claims, "aud") && assert.Contains(t, claims, "email")
}
p := newFakeProxy(cfg)
p.idp.setTokenExpiration(1000 * time.Millisecond)
......@@ -1096,6 +1178,7 @@ func TestCheckRefreshTokens(t *testing.T) {
ExpectedProxy: true,
ExpectedCode: http.StatusOK,
ExpectedCookies: map[string]string{cfg.CookieAccessName: ""},
ExpectedCookiesValidator: map[string]func(string) bool{cfg.CookieAccessName: val},
},
}
p.RunTests(t, requests)
......
......@@ -33,7 +33,7 @@ func (r *oauthProxy) getIdentity(req *http.Request) (*userContext, error) {
if err != nil {
return nil, err
}
if r.config.EnableEncryptedToken {
if r.config.EnableEncryptedToken || r.config.ForceEncryptedCookie && !isBearer {
if access, err = decodeText(access, r.config.EncryptionKey); err != nil {
return nil, ErrDecryption
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment