diff --git a/handlers.go b/handlers.go index 99847e591e13fd33f4891fdbf9133db552528800..c3cb58c03d7b3f1736641426394f9c87607d23b2 100644 --- a/handlers.go +++ b/handlers.go @@ -32,105 +32,131 @@ import ( // c) proxyHandler is responsible for handling the reverse proxy to the upstream endpoint // -/* -// loggingHandler is logging middleware -func (r *KeycloakProxy) loggingHandler() gin.HandlerFunc { +const ( + authRequired = "AUTH_REQUIRED" +) + +// entrypointHandler checks to see if the request requires authentication +func (r *KeycloakProxy) entrypointHandler() gin.HandlerFunc { return func(cx *gin.Context) { - req, err := httputil.DumpRequest(cx.Request, true) - if err == nil { - glog.V(10).Infof("%s", req) + glog.V(10).Infof("entering the entrypoint handler, uri: %s", cx.Request.RequestURI) + + // check if authentication is required + for _, resource := range r.config.Resources { + if strings.HasPrefix(cx.Request.RequestURI, resource.URL) { + if containedIn(cx.Request.Method, resource.Methods) { + cx.Set(authRequired, true) + } else if containedIn("ANY", resource.Methods) { + cx.Set(authRequired, true) + } + + break + } } } } -*/ // authenticationHandler is responsible for verifying the access token -func (r *KeycloakProxy) authenticationHandler(cx *gin.Context) { - glog.V(10).Infof("entering the authentication handler, uri: %s", cx.Request.RequestURI) - - // step: extract the token if there is one - // a) if there is no token, we check for session state and if so, we try to refresh the token - // b) there is no token or session state, we simple redirect to keycloak - session, err := r.getSessionToken(cx) - if err != nil { - // step: there isn't a session cookie, do we have refresh session cookie? - if err == ErrSessionNotFound && r.config.RefreshSession { - session, err = r.refreshUserSessionToken(cx) - if err != nil { - glog.Errorf("failed to refresh the access token, reason: %s", err) - r.redirectToAuthorization(cx) - return - } - } else { - glog.Errorf("failed to get session redirecting for authorization") - r.redirectToAuthorization(cx) +func (r *KeycloakProxy) authenticationHandler() gin.HandlerFunc { + return func(cx *gin.Context) { + glog.V(10).Infof("entering the authentication handler, uri: %s", cx.Request.RequestURI) + + // step: is authentication required on this + if _, found := cx.Get(authRequired); !found { return } - } - // step: retrieve the identity and inject in the context - userContext, err := r.getUserContext(session) - if err != nil { - glog.Errorf("failed to retrieve the identity from the token, reason: %s", err) - r.redirectToAuthorization(cx) - return - } - cx.Set(userContextName, userContext) - - // step: verify the access token - if err := r.verifyToken(userContext.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 - if err != ErrAccessTokenExpired { - glog.Errorf("invalid access token, %s, reason: %s", userContext.token, err) - r.accessForbidden(cx) - return + // step: extract the token if there is one + // a) if there is no token, we check for session state and if so, we try to refresh the token + // b) there is no token or session state, we simple redirect to keycloak + session, err := r.getSessionToken(cx) + if err != nil { + // step: there isn't a session cookie, do we have refresh session cookie? + if err == ErrSessionNotFound && r.config.RefreshSession { + session, err = r.refreshUserSessionToken(cx) + if err != nil { + glog.Errorf("failed to refresh the access token, reason: %s", err) + r.redirectToAuthorization(cx) + return + } + } else { + glog.Errorf("failed to get session redirecting for authorization") + r.redirectToAuthorization(cx) + return + } } - // step: are we refreshing the access tokens? - if !r.config.RefreshSession { - glog.Errorf("the session has expired for user: %s and token refreshing is disabled", userContext) + // step: retrieve the identity and inject in the context + userContext, err := r.getUserContext(session) + if err != nil { + glog.Errorf("failed to retrieve the identity from the token, reason: %s", err) r.redirectToAuthorization(cx) return } + cx.Set(userContextName, userContext) + + // step: verify the access token + if err := r.verifyToken(userContext.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 + if err != ErrAccessTokenExpired { + glog.Errorf("invalid access token, %s, reason: %s", userContext.token, err) + r.accessForbidden(cx) + return + } - // step: attempt to refresh the access token - if _, err := r.refreshUserSessionToken(cx); err != nil { - glog.Errorf("failed to refresh the access token, reason: %s", err) - r.redirectToAuthorization(cx) - return + // step: are we refreshing the access tokens? + if !r.config.RefreshSession { + glog.Errorf("the session has expired for user: %s and token refreshing is disabled", userContext) + r.redirectToAuthorization(cx) + return + } + + // step: attempt to refresh the access token + if _, err := r.refreshUserSessionToken(cx); err != nil { + glog.Errorf("failed to refresh the access token, reason: %s", err) + r.redirectToAuthorization(cx) + return + } } } } // admissionHandler is responsible checking the access token against the protected resource -func (r *KeycloakProxy) admissionHandler(cx *gin.Context) { - // step: grab the identity from the context - userContext, found := cx.Get(userContextName) - if !found { - panic("there is no identity in the request context") - } +func (r *KeycloakProxy) admissionHandler() gin.HandlerFunc { + return func(cx *gin.Context) { + // step: is authentication required on this + if _, found := cx.Get(authRequired); !found { + return + } - identity := userContext.(*UserContext) + // step: grab the identity from the context + userContext, found := cx.Get(userContextName) + if !found { + panic("there is no identity in the request context") + } - // step: validate the roles assigned to this token is valid for the resource - for _, resource := range r.config.Resources { - // step: check if it starts with the resource prefix - if strings.HasPrefix(cx.Request.RequestURI, resource.URL) { - // step: do we have any roles or do we need authentication only - if len(resource.RolesAllowed) <= 0 { - glog.V(4).Infof("[allowed] resource: %s authentication only, expires in: %s", resource, identity.expiresAt.Sub(time.Now())) + identity := userContext.(*UserContext) + + // step: validate the roles assigned to this token is valid for the resource + for _, resource := range r.config.Resources { + // step: check if it starts with the resource prefix + if strings.HasPrefix(cx.Request.RequestURI, resource.URL) { + // step: do we have any roles or do we need authentication only + if len(resource.RolesAllowed) <= 0 { + glog.V(4).Infof("[allowed] resource: %s authentication only, expires in: %s", resource, identity.expiresAt.Sub(time.Now())) + return + } + // step: we need to check the roles + if !hasRoles(resource.RolesAllowed, identity.roles) { + glog.Errorf("[denied] resource: %s invalid roles, issued: %s", resource, identity.roles) + r.accessForbidden(cx) + return + } + + glog.V(10).Infof("[allowed] resource: %s, expires in: %s", resource, identity.expiresAt.Sub(time.Now())) return } - // step: we need to check the roles - if !hasRoles(resource.RolesAllowed, identity.roles) { - glog.Errorf("[denied] resource: %s invalid roles, issued: %s", resource, identity.roles) - r.accessForbidden(cx) - return - } - - glog.V(10).Infof("[allowed] resource: %s, expires in: %s", resource, identity.expiresAt.Sub(time.Now())) - return } } } diff --git a/server.go b/server.go index ff56ff6e729ea7a4e6aa982955fe373a9bc2ee37..eda112d048bae0d523063657be5bd6130b72acd3 100644 --- a/server.go +++ b/server.go @@ -19,7 +19,6 @@ import ( "fmt" "net/http" "net/url" - "strings" "sync" "github.com/gin-gonic/gin" @@ -40,7 +39,8 @@ func NewProxy(cfg *Config) (*KeycloakProxy, error) { } // step: initialize the openid client - client, clientCfg, err := initializeOpenID(cfg.DiscoveryURL, cfg.ClientID, cfg.Secret, cfg.Scopes) + client, clientCfg, err := initializeOpenID(cfg.DiscoveryURL, + cfg.ClientID, cfg.Secret, cfg.RedirectionURL, cfg.Scopes) if err != nil { return nil, err } @@ -59,11 +59,9 @@ func NewProxy(cfg *Config) (*KeycloakProxy, error) { service.router = gin.Default() for _, resource := range cfg.Resources { - glog.Infof("protecting resource: %s", resource) - for _, method := range resource.Methods { - service.router.Handle(strings.ToUpper(method), resource.URL, service.authenticationHandler, service.admissionHandler) - } + glog.Infof("protecting resources under: %s", resource) } + service.router.Use(service.entrypointHandler(), service.authenticationHandler(), service.admissionHandler()) // step: add the oauth handlers and health service.router.GET(authorizationURL, service.authorizationHandler) diff --git a/session.go b/session.go index 80b5d42c0d022ef2928b20bf4d03e5f4af70000b..dd604d9556f1e216f56c4904ab6f4af0e4a81fc8 100644 --- a/session.go +++ b/session.go @@ -105,7 +105,7 @@ func (r *KeycloakProxy) getUserContext(token jose.JWT) (*UserContext, error) { } // step: get the preferred name - preferredName, _, err := claims.StringClaim("preferred_name") + preferredName, _, err := claims.StringClaim("preferred_username") if err != nil { glog.Warningf("unable to extract the preferred name from the token claims, reason: %s", err) } diff --git a/session_test.go b/session_test.go index 696b8db49b4b3f67bff43c72cdce266fe0a29e04..65652fc9d7c05ff8ed42267128ce4e8d88b72014 100644 --- a/session_test.go +++ b/session_test.go @@ -18,12 +18,62 @@ package main import ( "testing" "time" + "reflect" "github.com/stretchr/testify/assert" + "github.com/gambol99/go-oidc/jose" ) func TestGetUserContext(t *testing.T) { + proxy := newFakeKeycloakProxy(t) + + testToken, err := jose.NewJWT( + jose.JOSEHeader{ + "alg": "RS256", + }, + jose.Claims{ + "jti": "4ee75b8e-3ee6-4382-92d4-3390b4b4937b", + //"exp": "1450372969", + "nbf": 0, + "iat": "1450372669", + "iss": "https://keycloak.example.com/auth/realms/commons", + "aud": "test", + "sub": "1e11e539-8256-4b3b-bda8-cc0d56cddb48", + "typ": "Bearer", + "azp": "clientid", + "session_state": "98f4c3d2-1b8c-4932-b8c4-92ec0ea7e195", + "client_session": "f0105893-369a-46bc-9661-ad8c747b1a69", + "resource_access": map[string]interface{}{ + "openvpn": map[string]interface{}{ + "roles": []string{ + "dev-vpn", + }, + }, + }, + "email": "gambol99@gmail.com", + "name": "Rohith Jayawardene", + "family_name": "Jayawardene", + "preferred_username": "rjayawardene", + "given_name": "Rohith", + }) + + if assert.NoError(t, err, "should not have recieved an error parsing the token") { + t.Failed() + } + if !assert.NotNil(t, testToken, "should not have got nil from token") { + t.FailNow() + } + context, err := proxy.getUserContext(testToken) + assert.NoError(t, err) + assert.NotNil(t, context) + assert.Equal(t, "1e11e539-8256-4b3b-bda8-cc0d56cddb48", context.id) + assert.Equal(t, "gambol99@gmail.com", context.email) + assert.Equal(t, "rjayawardene", context.preferredName) + roles := []string{"openvpn:dev-vpn"} + if !reflect.DeepEqual(context.roles, roles) { + t.Errorf("the claims are not the same, %v <-> %v", context.roles, roles) + } } func TestEncodeState(t *testing.T) { diff --git a/utils.go b/utils.go index c2ff3e1d096b4fa990cf5a1fd3cede44bf6ee558..743154a5bf65ec27f7530aa26595383fb56658a3 100644 --- a/utils.go +++ b/utils.go @@ -41,7 +41,7 @@ import ( ) var ( - httpMethodRegex = regexp.MustCompile("^(GET|POST|DELETE|PATCH|HEAD|PUT|TRACE|CONNECT)$") + httpMethodRegex = regexp.MustCompile("^(ANY|GET|POST|DELETE|PATCH|HEAD|PUT|TRACE|CONNECT)$") ) // encryptDataBlock encrypts the plaintext string with the key @@ -88,7 +88,7 @@ func decryptDataBlock(cipherText, key []byte) ([]byte, error) { // initializeOpenID initializes the openID configuration, note: the redirection url is deliberately left blank // in order to retrieve it from the host header on request -func initializeOpenID(discoveryURL, clientID, clientSecret string, scopes []string) (*oidc.Client, oidc.ClientConfig, error) { +func initializeOpenID(discoveryURL, clientID, clientSecret, redirectURL string, scopes []string) (*oidc.Client, oidc.ClientConfig, error) { var err error var providerConfig oidc.ProviderConfig @@ -116,7 +116,7 @@ func initializeOpenID(discoveryURL, clientID, clientSecret string, scopes []stri ID: clientID, Secret: clientSecret, }, - RedirectURL: "http://127.0.0.1:3000/oauth/callback", + RedirectURL: fmt.Sprintf("%s/oauth/callback", redirectURL), Scope: append(scopes, oidc.DefaultScope...), }