Skip to content
This repository has been archived by the owner on Dec 7, 2020. It is now read-only.

Commit

Permalink
Access Token Duration (#188)
Browse files Browse the repository at this point in the history
* Access Token Duration

- fixed the access token expiring too quickly; the cookie expiration needed to be extended.
  Not sure when this was broken, but I changed the duration a while back. Needs a test added.

* - switching to gambol99/goproxy until a fix upstream
  • Loading branch information
gambol99 authored Feb 5, 2017
1 parent ea960a9 commit 72ce41a
Show file tree
Hide file tree
Showing 26 changed files with 73 additions and 82 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ CHANGES:
* Fixed up some spelling mistakes [#PR177](https://github.com/gambol99/keycloak-proxy/pull/177)
* Changed the CLI to use reflection of the config struct [#PR176](https://github.com/gambol99/keycloak-proxy/pull/176)
* Updated the docker base image to alpine:3.5 [#PR184](https://github.com/gambol99/keycloak-proxy/pull/184)
* Added a new options to control the access token duration [#PR188](https://github.com/gambol99/keycloak-proxy/pull/188)

BUGS:
* Fixed the time.Duration flags in the reflection code [#PR173](https://github.com/gambol99/keycloak-proxy/pull/173)
* Fixed the environment variable type [#PR176](https://github.com/gambol99/keycloak-proxy/pull/176)
* Fixed the refresh tokens, the access token cookie was timing out too quickly ([#PR188](https://github.com/gambol99/keycloak-proxy/pull/188)

#### **2.0.1**

Expand Down
4 changes: 2 additions & 2 deletions Godeps/Godeps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
// newDefaultConfig returns a initialized config
func newDefaultConfig() *Config {
return &Config{
AccessTokenDuration: time.Duration(720) * time.Hour,
Tags: make(map[string]string, 0),
MatchClaims: make(map[string]string, 0),
Headers: make(map[string]string, 0),
Expand Down
2 changes: 2 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ type Config struct {
// LocalhostMetrics indicated the metrics can only be consume via localhost
LocalhostMetrics bool `json:"localhost-metrics" yaml:"localhost-metrics" usage:"enforces the metrics page can only been requested from 127.0.0.1"`

// AccessTokenDuration is default duration applied to the access token cookie
AccessTokenDuration time.Duration `json:"access-token-duration" yaml:"access-token-duration" usage:"fallback cookie duration for the access token when using refresh tokens"`
// CookieDomain is a list of domains the cookie is available to
CookieDomain string `json:"cookie-domain" yaml:"cookie-domain" usage:"domain the access cookie is available to, defaults host header"`
// CookieAccessName is the name of the access cookie holding the access token
Expand Down
11 changes: 3 additions & 8 deletions forwarding.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ import (
"github.com/gin-gonic/gin"
)

//
// reverseProxyMiddleware is responsible for handles reverse proxy request to the upstream endpoint
//
func (r *oauthProxy) reverseProxyMiddleware() gin.HandlerFunc {
return func(cx *gin.Context) {
if cx.IsAborted() {
Expand All @@ -46,10 +44,9 @@ func (r *oauthProxy) reverseProxyMiddleware() gin.HandlerFunc {
cx.Abort()
return
}
/*
By default goproxy only provides a forwarding proxy, thus all requests have to be absolute
and we must update the host headers
*/

// By default goproxy only provides a forwarding proxy, thus all requests have to be absolute
// and we must update the host headers
cx.Request.URL.Host = r.endpoint.Host
cx.Request.URL.Scheme = r.endpoint.Scheme
cx.Request.Host = r.endpoint.Host
Expand All @@ -58,9 +55,7 @@ func (r *oauthProxy) reverseProxyMiddleware() gin.HandlerFunc {
}
}

//
// forwardProxyHandler is responsible for signing outbound requests
//
func (r *oauthProxy) forwardProxyHandler() func(*http.Request, *http.Response) {
// step: create oauth client
client, err := r.client.OAuthClient()
Expand Down
45 changes: 19 additions & 26 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func (r *oauthProxy) oauthCallbackHandler(cx *gin.Context) {
}

// step: exchange the authorization for a access token
response, err := exchangeAuthenticationCode(client, code)
resp, err := exchangeAuthenticationCode(client, code)
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to exchange code for access token")

Expand All @@ -134,7 +134,7 @@ func (r *oauthProxy) oauthCallbackHandler(cx *gin.Context) {
}

// step: parse decode the identity token
session, identity, err := parseToken(response.IDToken)
token, identity, err := parseToken(resp.IDToken)
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to parse id token for identity")

Expand All @@ -143,64 +143,58 @@ func (r *oauthProxy) oauthCallbackHandler(cx *gin.Context) {
}

// step: verify the token is valid
if err = verifyToken(r.client, session); err != nil {
if err = verifyToken(r.client, token); err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to verify the id token")

r.accessForbidden(cx)
return
}

// step: attempt to decode the access token else we default to the id token
accessToken, id, err := parseToken(response.AccessToken)
access, id, err := parseToken(resp.AccessToken)
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to parse the access token, using id token only")
} else {
session = accessToken
token = access
identity = id
}

log.WithFields(log.Fields{
"email": identity.Email,
"expires": identity.ExpiresAt.Format(time.RFC3339),
"duration": identity.ExpiresAt.Sub(time.Now()).String(),
}).Infof("issuing a new access token for user, email: %s", identity.Email)

// step: drop's a session cookie with the access token
duration := identity.ExpiresAt.Sub(time.Now())
r.dropAccessTokenCookie(cx, session.Encode(), duration)
}).Infof("issuing access token for user, email: %s", identity.Email)

// step: does the response has a refresh token and we are NOT ignore refresh tokens?
if r.config.EnableRefreshTokens && response.RefreshToken != "" {
if r.config.EnableRefreshTokens && resp.RefreshToken != "" {
// step: encrypt the refresh token
encrypted, err := encodeText(response.RefreshToken, r.config.EncryptionKey)
encrypted, err := encodeText(resp.RefreshToken, r.config.EncryptionKey)
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to encrypt the refresh token")

cx.AbortWithStatus(http.StatusInternalServerError)
return
}

// step: create and inject the state session
// drop in the access token - cookie expiration = access token
r.dropAccessTokenCookie(cx, token.Encode(), r.getAccessCookieExpiration(token, resp.RefreshToken))

switch r.useStore() {
case true:
if err := r.StoreRefreshToken(session, encrypted); err != nil {
if err := r.StoreRefreshToken(token, encrypted); err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Warnf("failed to save the refresh token in the store")
}
// step: get expiration of the refresh token if we can
_, ident, err := parseToken(response.RefreshToken)
if err != nil {
r.dropAccessTokenCookie(cx, session.Encode(), time.Duration(72)*time.Hour)
} else {
r.dropAccessTokenCookie(cx, session.Encode(), ident.ExpiresAt.Sub(time.Now()))
}
default:
// step: attempt to decode the refresh token (not all refresh tokens are jwt tokens; google for instance.
if _, ident, err := parseToken(response.RefreshToken); err != nil {
r.dropRefreshTokenCookie(cx, encrypted, time.Duration(72)*time.Hour)
// notes: not all idp refresh tokens are readable, google for example, so we attempt to decode into
// a jwt and if possible extract the expiration, else we default to 10 days
if _, ident, err := parseToken(resp.RefreshToken); err != nil {
r.dropRefreshTokenCookie(cx, encrypted, time.Duration(240)*time.Hour)
} else {
r.dropRefreshTokenCookie(cx, encrypted, ident.ExpiresAt.Sub(time.Now()))
}
}
} else {
r.dropAccessTokenCookie(cx, token.Encode(), identity.ExpiresAt.Sub(time.Now()))
}

// step: decode the state variable
Expand Down Expand Up @@ -231,7 +225,6 @@ func (r *oauthProxy) loginHandler(cx *gin.Context) {
// step: parse the client credentials
username := cx.Request.PostFormValue("username")
password := cx.Request.PostFormValue("password")

if username == "" || password == "" {
return "request does not have both username and password", http.StatusBadRequest, errors.New("no credentials")
}
Expand Down Expand Up @@ -285,7 +278,7 @@ func (r *oauthProxy) loginHandler(cx *gin.Context) {
// - optionally, the user can be redirected by to a url
//
func (r *oauthProxy) logoutHandler(cx *gin.Context) {
// the user can specify a url to redirect the back to
// the user can specify a url to redirect the back
redirectURL := cx.Request.URL.Query().Get("redirect")

// step: drop the access token
Expand Down
69 changes: 28 additions & 41 deletions middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (r *oauthProxy) metricsMiddleware() gin.HandlerFunc {
// entrypointMiddleware checks to see if the request requires authentication
func (r *oauthProxy) entrypointMiddleware() gin.HandlerFunc {
return func(cx *gin.Context) {
// step: we can skip the
// step: we can skip if under oauth prefix
if strings.HasPrefix(cx.Request.URL.Path, oauthURL) {
return
}
Expand Down Expand Up @@ -128,7 +128,7 @@ func (r *oauthProxy) authenticationMiddleware() gin.HandlerFunc {
// step: inject the user into the context
cx.Set(userContextName, user)

// step: verify the access token
// step: skipif we are running skip-token-verification
if r.config.SkipTokenVerification {
log.Warnf("skip token verification enabled, skipping verification process - FOR TESTING ONLY")

Expand All @@ -145,7 +145,6 @@ func (r *oauthProxy) authenticationMiddleware() gin.HandlerFunc {
return
}

// step: verify the access token
if err := verifyToken(r.client, user.token); err != nil {
// step: if the error post verification is anything other than a token expired error
// we immediately throw an access forbidden - as there is something messed up in the token
Expand Down Expand Up @@ -174,78 +173,68 @@ func (r *oauthProxy) authenticationMiddleware() gin.HandlerFunc {
log.WithFields(log.Fields{
"email": user.email,
"client_ip": clientIP,
}).Infof("accces token for user: %s has expired, attemping to refresh the token", user.email)
}).Infof("accces token for user has expired, attemping to refresh the token")

// step: check if the user has refresh token
rToken, err := r.retrieveRefreshToken(cx.Request, user)
refresh, err := r.retrieveRefreshToken(cx.Request, user)
if err != nil {
log.WithFields(log.Fields{
"email": user.email,
"error": err.Error(),
"client_ip": clientIP,
}).Errorf("unable to find a refresh token for the client: %s", user.email)
}).Errorf("unable to find a refresh token for user")

r.redirectToAuthorization(cx)
return
}

log.WithFields(log.Fields{
"email": user.email,
"client_ip": clientIP,
}).Info("attempting to refresh access token for user")

token, expires, err := getRefreshedToken(r.client, rToken)
// attempt to refresh the access token
token, _, err := getRefreshedToken(r.client, refresh)
if err != nil {
// step: has the refresh token expired?
switch err {
case ErrRefreshTokenExpired:
log.WithFields(log.Fields{
"email": user.email,
"client_ip": clientIP,
}).Warningf("refresh token has expired for user")
}).Warningf("refresh token has expired, cannot retrieve access token")

r.clearAllCookies(cx)
default:
log.WithFields(log.Fields{
"error": err.Error(),
}).Errorf("failed to refresh the access token")
log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to refresh the access token")
}

r.redirectToAuthorization(cx)
return
}

// step: inject the refreshed access token
accessDuration := expires.Sub(time.Now())
// get the expiration of the new access token
expiresIn := r.getAccessCookieExpiration(token, refresh)

log.WithFields(log.Fields{
"email": user.email,
"expires_in": accessDuration.String(),
"client_ip": clientIP,
}).Infof("injecting refreshed access token, expires on: %s", expires.Format(time.RFC3339))
"client_ip": clientIP,
"cookie_name": r.config.CookieAccessName,
"email": user.email,
"expires_in": expiresIn.String(),
}).Infof("injecting the refreshed access token cookie")

// step: drop's a session cookie with the access token
duration := expires.Sub(time.Now())
if r.useStore() {
duration = duration * 10
}

r.dropAccessTokenCookie(cx, token.Encode(), duration)
// step: inject the refreshed access token
r.dropAccessTokenCookie(cx, token.Encode(), expiresIn)

if r.useStore() {
go func(t jose.JWT, rt string) {
// step: store the new refresh token
if err := r.StoreRefreshToken(t, rt); err != nil {
log.WithFields(log.Fields{
"error": err.Error(),
}).Errorf("failed to store refresh token")

go func(old, new jose.JWT, state string) {
if err := r.DeleteRefreshToken(old); err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to remove old token")
}
if err := r.StoreRefreshToken(new, state); err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to store refresh token")
return
}
}(user.token, rToken)
}(user.token, token, refresh)
}

// step: update the with the new access token
user.token = token

// step: inject the user into the context
cx.Set(userContextName, user)
}
Expand Down Expand Up @@ -438,9 +427,7 @@ func (r *oauthProxy) securityMiddleware() gin.HandlerFunc {

return func(cx *gin.Context) {
if err := secure.Process(cx.Writer, cx.Request); err != nil {
log.WithFields(log.Fields{
"error": err.Error(),
}).Errorf("failed security middleware")
log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed security middleware")

cx.Abort()
}
Expand Down
15 changes: 15 additions & 0 deletions misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import (
"fmt"
"net/http"
"path"
"time"

log "github.com/Sirupsen/logrus"
"github.com/coreos/go-oidc/jose"
"github.com/gin-gonic/gin"
)

Expand Down Expand Up @@ -62,3 +64,16 @@ func (r *oauthProxy) redirectToAuthorization(cx *gin.Context) {

r.redirectToURL(oauthURL+authorizationURL+authQuery, cx)
}

// getAccessCookieExpiration calucates the expiration of the access token cookie
func (r *oauthProxy) getAccessCookieExpiration(token jose.JWT, refresh string) time.Duration {
// notes: by default the duration of the access token will be the configuration option, if
// however we can decode the refresh token, we will set the duration to the duraction of the
// refresh token
duration := r.config.AccessTokenDuration
if _, ident, err := parseToken(refresh); err == nil {
duration = ident.ExpiresAt.Sub(time.Now())
}

return duration
}
3 changes: 1 addition & 2 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import (
log "github.com/Sirupsen/logrus"
"github.com/armon/go-proxyproto"
"github.com/coreos/go-oidc/oidc"
"github.com/elazarl/goproxy"
"github.com/gambol99/goproxy"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
)
Expand Down Expand Up @@ -238,7 +238,6 @@ func (r *oauthProxy) createForwardingProxy() error {

// step: setup the tls configuration
if r.config.TLSCaCertificate != "" && r.config.TLSCaPrivateKey != "" {
// step: read in the ca
ca, err := loadCA(r.config.TLSCaCertificate, r.config.TLSCaPrivateKey)
if err != nil {
return fmt.Errorf("unable to load certificate authority, error: %s", err)
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 72ce41a

Please sign in to comment.