Skip to content
Snippets Groups Projects
Commit 2ac8f7ee authored by Rohith's avatar Rohith
Browse files

- fixed the typo on the readme and google example

- cleaned up some of the logging
- fixed the refresh tokens for providers that dont use jwt i.e. google
- ensure all the time is UTC relative
- cleanup a few issues where i saw them
parent c867d4db
No related branches found
No related tags found
No related merge requests found
#### **1.0.1 (April 8th, 2016)**
FIXES:
* Fixed the refresh tokens for those provides whom do not use JWT tokens, Google Connect for example
#### **1.0.0 (April 8th, 2016)** #### **1.0.0 (April 8th, 2016)**
FEATURES FEATURES
......
...@@ -176,7 +176,7 @@ Although the role extensions do require a Keycloak IDP or at the very least a ID ...@@ -176,7 +176,7 @@ Although the role extensions do require a Keycloak IDP or at the very least a ID
``` shell ``` shell
bin/keycloak-proxy \ bin/keycloak-proxy \
--discovery-url=https://accounts.google.com/.well-known/openid-confuration \ --discovery-url=https://accounts.google.com/.well-known/openid-configuration \
--client-id=<CLIENT_ID> \ --client-id=<CLIENT_ID> \
--secret=<CLIENT_SECRET> \ --secret=<CLIENT_SECRET> \
--resource="uri=/" \ --resource="uri=/" \
......
...@@ -11,7 +11,7 @@ listen: 127.0.0.1:3000 ...@@ -11,7 +11,7 @@ listen: 127.0.0.1:3000
# whether to request offline access and use a refresh token # whether to request offline access and use a refresh token
refresh_sessions: true refresh_sessions: true
# assuming you are using refresh tokens, specify the maximum amount of time the refresh token can last # assuming you are using refresh tokens, specify the maximum amount of time the refresh token can last
max_session: 1h max-session: 1h
# log all incoming requests # log all incoming requests
log_requests: true log_requests: true
# log in json format # log in json format
......
...@@ -22,7 +22,7 @@ import ( ...@@ -22,7 +22,7 @@ import (
const ( const (
prog = "keycloak-proxy" prog = "keycloak-proxy"
version = "v1.0.0" version = "v1.0.1"
author = "Rohith" author = "Rohith"
email = "gambol99@gmail.com" email = "gambol99@gmail.com"
description = "is a proxy using the keycloak service for auth and authorization" description = "is a proxy using the keycloak service for auth and authorization"
...@@ -108,7 +108,7 @@ type Config struct { ...@@ -108,7 +108,7 @@ type Config struct {
// EncryptionKey is the encryption key used to encrypt the refresh token // EncryptionKey is the encryption key used to encrypt the refresh token
EncryptionKey string `json:"encryption_key" yaml:"encryption_key"` EncryptionKey string `json:"encryption_key" yaml:"encryption_key"`
// MaxSession the max session for refreshing // MaxSession the max session for refreshing
MaxSession time.Duration `json:"max_session" yaml:"max_session"` MaxSession time.Duration `json:"max-session" yaml:"max-session"`
// ClaimsMatch is a series of checks, the claims in the token must match those here // ClaimsMatch is a series of checks, the claims in the token must match those here
ClaimsMatch map[string]string `json:"claims" yaml:"claims"` ClaimsMatch map[string]string `json:"claims" yaml:"claims"`
// Keepalives specifies wheather we use keepalives on the upstream // Keepalives specifies wheather we use keepalives on the upstream
......
...@@ -152,6 +152,7 @@ func (r *KeycloakProxy) authenticationHandler() gin.HandlerFunc { ...@@ -152,6 +152,7 @@ func (r *KeycloakProxy) authenticationHandler() gin.HandlerFunc {
if _, found := cx.Get(cxEnforce); !found { if _, found := cx.Get(cxEnforce); !found {
log.Debugf("skipping the authentication handler, resource not protected") log.Debugf("skipping the authentication handler, resource not protected")
cx.Next() cx.Next()
return return
} }
...@@ -167,7 +168,7 @@ func (r *KeycloakProxy) authenticationHandler() gin.HandlerFunc { ...@@ -167,7 +168,7 @@ func (r *KeycloakProxy) authenticationHandler() gin.HandlerFunc {
return return
} }
} else { } else {
log.Errorf("failed to get session redirecting for authorization, %s", err) log.Errorf("failed to get session, redirecting for authorization, %s", err)
r.redirectToAuthorization(cx) r.redirectToAuthorization(cx)
return return
} }
...@@ -422,33 +423,32 @@ func (r *KeycloakProxy) oauthAuthorizationHandler(cx *gin.Context) { ...@@ -422,33 +423,32 @@ func (r *KeycloakProxy) oauthAuthorizationHandler(cx *gin.Context) {
return return
} }
log.WithFields(log.Fields{
"client_ip": cx.ClientIP(),
}).Infof("incoming authorization request")
// step: grab the oauth client // step: grab the oauth client
oac, err := r.openIDClient.OAuthClient() oac, err := r.openIDClient.OAuthClient()
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to retrieve the oauth client")
"error": err.Error(),
}).Errorf("failed to retrieve the oauth client")
cx.AbortWithStatus(http.StatusInternalServerError) cx.AbortWithStatus(http.StatusInternalServerError)
return return
} }
// step: get the access type required // step: set the grant type of the session
accessType := "" accessType := ""
if r.config.RefreshSessions { if r.config.RefreshSessions {
accessType = "offline" accessType = "offline"
} }
log.WithFields(log.Fields{
"client_ip": cx.ClientIP(),
"access_type": accessType,
}).Infof("incoming authorization request from client address: %s", cx.ClientIP())
// step: build the redirection url to the authentication server // step: build the redirection url to the authentication server
redirectionURL := oac.AuthCodeURL(cx.Query("state"), accessType, "") redirectionURL := oac.AuthCodeURL(cx.Query("state"), accessType, "")
// step: if we have a custom sign in page, lets display that // step: if we have a custom sign in page, lets display that
if r.config.hasSignInPage() { if r.config.hasSignInPage() {
// add the redirection url // step: add the redirection url
model := make(map[string]string, 0) model := make(map[string]string, 0)
for k, v := range r.config.TagData { for k, v := range r.config.TagData {
model[k] = v model[k] = v
...@@ -466,7 +466,6 @@ func (r *KeycloakProxy) oauthAuthorizationHandler(cx *gin.Context) { ...@@ -466,7 +466,6 @@ func (r *KeycloakProxy) oauthAuthorizationHandler(cx *gin.Context) {
// //
// oauthCallbackHandler is responsible for handling the response from keycloak // oauthCallbackHandler is responsible for handling the response from keycloak
// //
// @@TODO need to clean up this method somewhat
func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) { func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) {
// step: is token verification switched on? // step: is token verification switched on?
if r.config.SkipTokenVerification { if r.config.SkipTokenVerification {
...@@ -474,17 +473,19 @@ func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) { ...@@ -474,17 +473,19 @@ func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) {
return return
} }
// step: ensure we have a authorization code to exchange // step: get the code and state
code := cx.Request.URL.Query().Get("code") code := cx.Request.URL.Query().Get("code")
state := cx.Request.URL.Query().Get("state")
// step: ensure we have a authorization code to exchange
if code == "" { if code == "" {
log.WithFields(log.Fields{"client_ip": cx.ClientIP()}).Error("code parameter not found in callback request") log.WithFields(log.Fields{"client_ip": cx.ClientIP()}).Error("code parameter missing in callback")
r.accessForbidden(cx) r.accessForbidden(cx)
return return
} }
// step: grab the state from request, otherwise default to root url // step: ensure we have a state or default to root /
state := cx.Request.URL.Query().Get("state")
if state == "" { if state == "" {
state = "/" state = "/"
} }
...@@ -497,13 +498,14 @@ func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) { ...@@ -497,13 +498,14 @@ func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) {
return return
} }
// step: decode and verify the id token // step: parse decode the identity token
token, identity, err := r.parseToken(response.IDToken) token, identity, err := r.parseToken(response.IDToken)
if err != nil { if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to parse id token for identity") log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to parse id token for identity")
r.accessForbidden(cx) r.accessForbidden(cx)
return return
} }
// step: verify the token is valid
if err := r.verifyToken(token); err != nil { if err := r.verifyToken(token); err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to verify the id token") log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to verify the id token")
r.accessForbidden(cx) r.accessForbidden(cx)
...@@ -526,46 +528,54 @@ func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) { ...@@ -526,46 +528,54 @@ func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) {
// step: create a session from the access token // step: create a session from the access token
if err := r.createSession(token, identity.ExpiresAt, cx); err != nil { if err := r.createSession(token, identity.ExpiresAt, cx); err != nil {
log.Errorf("failed to inject the session token, error: %s", err) log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to inject the session token")
cx.AbortWithStatus(http.StatusInternalServerError) cx.AbortWithStatus(http.StatusInternalServerError)
return
}
// step: do we have session data to persist?
if r.config.RefreshSessions {
// step: parse the token
_, ident, err := r.parseToken(response.RefreshToken)
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to parse the refresh token")
cx.AbortWithStatus(http.StatusInternalServerError)
return return
} }
log.WithFields(log.Fields{ // step: are we using refresh tokens?
"email": identity.Email, if r.config.RefreshSessions {
"expires": identity.ExpiresAt,
}).Infof("retrieved the refresh token for user")
// step: create the state session // step: create the state session
state := &sessionState{ state := &sessionState{
refreshToken: response.RefreshToken, refreshToken: response.RefreshToken,
expireOn: time.Now().Add(r.config.MaxSession),
} }
maxSession := time.Now().Add(r.config.MaxSession) // step: can we parse and extract the refresh token from the response
switch maxSession.After(ident.ExpiresAt) { // - note, the refresh token can be custom, i.e. doesn't have to be a jwt i.e. google for example
case true: _, refreshToken, err := r.parseToken(response.RefreshToken)
state.expireOn = ident.ExpiresAt if err != nil {
default: log.WithFields(log.Fields{
state.expireOn = maxSession "error": err.Error(),
}).Errorf("unable to parse refresh token (unknown format) using the as a static string")
} else {
// step: set the expiration of the refresh token.
// - first we check if the duration exceeds the expiration of the refresh token
if state.expireOn.After(refreshToken.ExpiresAt) {
log.WithFields(log.Fields{
"email": refreshToken.Email,
"max_session": r.config.MaxSession.String(),
"duration": state.expireOn.Format(time.RFC1123),
"refresh": refreshToken.ExpiresAt.Format(time.RFC1123),
}).Errorf("max session exceeds the expiration of the refresh token, defaulting to refresh token")
state.expireOn = refreshToken.ExpiresAt
} }
}
// step: create and inject the state session
if err := r.createSessionState(state, cx); err != nil { if err := r.createSessionState(state, cx); err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to inject the session state into request") log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to inject the session state into request")
cx.AbortWithStatus(http.StatusInternalServerError) cx.AbortWithStatus(http.StatusInternalServerError)
return return
} }
// step: some debugging is useful here
log.WithFields(log.Fields{
"email": identity.Email,
"client_ip": cx.ClientIP(),
"expires_in": state.expireOn.Sub(time.Now()).String(),
}).Infof("successfully retrieve refresh token for client: %s", identity.Email)
} }
r.redirectToURL(state, cx) r.redirectToURL(state, cx)
......
...@@ -54,6 +54,11 @@ type reverseProxy interface { ...@@ -54,6 +54,11 @@ type reverseProxy interface {
ServeHTTP(rw http.ResponseWriter, req *http.Request) ServeHTTP(rw http.ResponseWriter, req *http.Request)
} }
func init() {
// step: ensure all time is in UTC
time.LoadLocation("UTC")
}
// newKeycloakProxy create's a new keycloak proxy from configuration // newKeycloakProxy create's a new keycloak proxy from configuration
func newKeycloakProxy(cfg *Config) (*KeycloakProxy, error) { func newKeycloakProxy(cfg *Config) (*KeycloakProxy, error) {
// step: set the logging level // step: set the logging level
......
...@@ -44,8 +44,8 @@ type sessionState struct { ...@@ -44,8 +44,8 @@ type sessionState struct {
refreshToken string refreshToken string
} }
// refreshUserSessionToken is responsible for retrieving the session state cookie and attempting to // refreshUserSessionToken is responsible for retrieving the session state cookie and attempting to refresh the access
// refresh the access token for the user // token for the user
func (r *KeycloakProxy) refreshUserSessionToken(cx *gin.Context) (jose.JWT, error) { func (r *KeycloakProxy) refreshUserSessionToken(cx *gin.Context) (jose.JWT, error) {
// step: grab the session state cooke // step: grab the session state cooke
state, err := r.getSessionState(cx) state, err := r.getSessionState(cx)
...@@ -63,26 +63,25 @@ func (r *KeycloakProxy) refreshUserSessionToken(cx *gin.Context) (jose.JWT, erro ...@@ -63,26 +63,25 @@ func (r *KeycloakProxy) refreshUserSessionToken(cx *gin.Context) (jose.JWT, erro
token, expires, err := r.refreshAccessToken(state.refreshToken) token, expires, err := r.refreshAccessToken(state.refreshToken)
if err != nil { if err != nil {
// step: has the refresh token expired // step: has the refresh token expired
if err == ErrRefreshTokenExpired { switch err {
case ErrRefreshTokenExpired:
log.WithFields(log.Fields{"token": token}).Warningf("the refresh token has expired") log.WithFields(log.Fields{"token": token}).Warningf("the refresh token has expired")
// clear the session
clearSessionState(cx) clearSessionState(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")
}
return jose.JWT{}, err return jose.JWT{}, err
} }
// step: inject the refreshed access token // step: inject the refreshed access token
log.Infof("injecting the refreshed access token into seesion, expires on: %s", expires) log.WithFields(log.Fields{
"access_expires_in": expires.Sub(time.Now()).String(),
"refresh_expires_in": state.expireOn.Sub(time.Now()).String(),
}).Infof("injecting refreshed access token, expires on: %s", expires.Format(time.RFC1123))
// step: create the session // step: create the session
if err := r.createSession(token, expires, cx); err != nil { return token, r.createSession(token, expires, cx)
return token, err
}
return token, nil
} }
// getSessionToken retrieves the authentication cookie from the request and parse's into a JWT token // getSessionToken retrieves the authentication cookie from the request and parse's into a JWT token
...@@ -107,14 +106,10 @@ func (r *KeycloakProxy) getSessionToken(cx *gin.Context) (jose.JWT, bool, error) ...@@ -107,14 +106,10 @@ func (r *KeycloakProxy) getSessionToken(cx *gin.Context) (jose.JWT, bool, error)
} }
session = cookie.Value session = cookie.Value
} }
// step: parse the token response
// step: parse the token
jwt, err := jose.ParseJWT(session) jwt, err := jose.ParseJWT(session)
if err != nil {
return jose.JWT{}, isBearer, err
}
return jwt, isBearer, nil return jwt, isBearer, err
} }
// getSession is a helper methods for those whom dont care if it's a bearer token // getSession is a helper methods for those whom dont care if it's a bearer token
...@@ -132,7 +127,7 @@ func (r *KeycloakProxy) getSessionState(cx *gin.Context) (*sessionState, error) ...@@ -132,7 +127,7 @@ func (r *KeycloakProxy) getSessionState(cx *gin.Context) (*sessionState, error)
return nil, ErrNoCookieFound return nil, ErrNoCookieFound
} }
return r.decodeState(cookie.Value) return r.decryptStateSession(cookie.Value)
} }
// getUserContext parse the jwt token and extracts the various elements is order to construct // getUserContext parse the jwt token and extracts the various elements is order to construct
...@@ -208,7 +203,7 @@ func (r *KeycloakProxy) createSession(token jose.JWT, expires time.Time, cx *gin ...@@ -208,7 +203,7 @@ func (r *KeycloakProxy) createSession(token jose.JWT, expires time.Time, cx *gin
// createSessionState creates a session state cookie, used to hold the refresh cookie and the expiration time // createSessionState creates a session state cookie, used to hold the refresh cookie and the expiration time
func (r *KeycloakProxy) createSessionState(state *sessionState, cx *gin.Context) error { func (r *KeycloakProxy) createSessionState(state *sessionState, cx *gin.Context) error {
// step: we need to encode the state // step: we need to encode the state
encoded, err := r.encodeState(state) encoded, err := r.encryptStateSession(state)
if err != nil { if err != nil {
return err return err
} }
...@@ -218,8 +213,8 @@ func (r *KeycloakProxy) createSessionState(state *sessionState, cx *gin.Context) ...@@ -218,8 +213,8 @@ func (r *KeycloakProxy) createSessionState(state *sessionState, cx *gin.Context)
return nil return nil
} }
// encodeState encodes the session state information into a value for a cookie to consume // encryptStateSession encodes the session state information into a value for a cookie to consume
func (r *KeycloakProxy) encodeState(session *sessionState) (string, error) { func (r *KeycloakProxy) encryptStateSession(session *sessionState) (string, error) {
// step: encode the session into a string // step: encode the session into a string
encoded := fmt.Sprintf("%d|%s", session.expireOn.Unix(), session.refreshToken) encoded := fmt.Sprintf("%d|%s", session.expireOn.Unix(), session.refreshToken)
...@@ -232,8 +227,8 @@ func (r *KeycloakProxy) encodeState(session *sessionState) (string, error) { ...@@ -232,8 +227,8 @@ func (r *KeycloakProxy) encodeState(session *sessionState) (string, error) {
return base64.StdEncoding.EncodeToString(cipherText), nil return base64.StdEncoding.EncodeToString(cipherText), nil
} }
// decodeState decodes the session state cookie value // decryptStateSession decodes the session state cookie value
func (r *KeycloakProxy) decodeState(state string) (*sessionState, error) { func (r *KeycloakProxy) decryptStateSession(state string) (*sessionState, error) {
// step: decode the base64 encrypted cookie // step: decode the base64 encrypted cookie
cipherText, err := base64.StdEncoding.DecodeString(state) cipherText, err := base64.StdEncoding.DecodeString(state)
if err != nil { if err != nil {
......
...@@ -195,7 +195,7 @@ func TestEncodeState(t *testing.T) { ...@@ -195,7 +195,7 @@ func TestEncodeState(t *testing.T) {
expireOn: time.Now(), expireOn: time.Now(),
} }
session, err := proxy.encodeState(state) session, err := proxy.encryptStateSession(state)
assert.NotEmpty(t, session) assert.NotEmpty(t, session)
assert.NoError(t, err) assert.NoError(t, err)
} }
...@@ -211,14 +211,14 @@ func TestDecodeState(t *testing.T) { ...@@ -211,14 +211,14 @@ func TestDecodeState(t *testing.T) {
expireOn: fakeExpiresOn, expireOn: fakeExpiresOn,
} }
session, err := proxy.encodeState(state) session, err := proxy.encryptStateSession(state)
assert.NotEmpty(t, session) assert.NotEmpty(t, session)
if err != nil { if err != nil {
t.Errorf("the encodeState() should not have handed an error") t.Errorf("the encryptStateSession() should not have handed an error")
t.FailNow() t.FailNow()
} }
decoded, err := proxy.decodeState(session) decoded, err := proxy.decryptStateSession(session)
assert.NotNil(t, decoded, "the session should not have been nil") assert.NotNil(t, decoded, "the session should not have been nil")
if assert.NoError(t, err, "the decodeState() should not have thrown an error") { if assert.NoError(t, err, "the decodeState() should not have thrown an error") {
assert.Equal(t, fakeToken, decoded.refreshToken, "the token should been the same") assert.Equal(t, fakeToken, decoded.refreshToken, "the token should been the same")
......
...@@ -73,5 +73,5 @@ func (r userContext) isBearerToken() bool { ...@@ -73,5 +73,5 @@ func (r userContext) isBearerToken() bool {
func (r userContext) String() string { func (r userContext) String() string {
return fmt.Sprintf("user: %s, expires: %s, roles: %s", r.preferredName, r.expiresAt.String(), return fmt.Sprintf("user: %s, expires: %s, roles: %s", r.preferredName, r.expiresAt.String(),
strings.Join(r.roles, "")) strings.Join(r.roles, ","))
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment