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

- adding the ability to use bearer tokens via the Authorization: Bearer <TOKEN>

- cleaned up elements where i could find it
- probably looking to perform a big cleanup soon
- updating the readme to reflect the cheanges
parent 42a552fb
No related branches found
No related tags found
No related merge requests found
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
### **Keycloak Proxy** ### **Keycloak Proxy**
---- ----
Keycloak-proxy is a proxy service which at the risk of stating the obvious integrates with the [Keycloak](https://github.com/keycloak/keycloak) authentication service. The configuration and feature set is based on the actual java version of the [proxy](https://docs.jboss.org/keycloak/docs/1.1.0.Beta2/userguide/html/proxy.html). The Keycloak-proxy is a proxy service which at the risk of stating the obvious integrates with the [Keycloak](https://github.com/keycloak/keycloak) authentication service.
The configuration and feature set is based on the actual java version of the [proxy](https://docs.jboss.org/keycloak/docs/1.1.0.Beta2/userguide/html/proxy.html). The service
supports both access tokens in browser cookie or bearer tokens.
```shell ```shell
[jest@starfury keycloak-proxy]$ bin/keycloak-proxy help [jest@starfury keycloak-proxy]$ bin/keycloak-proxy help
......
...@@ -18,13 +18,14 @@ package main ...@@ -18,13 +18,14 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"gopkg.in/yaml.v2"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"gopkg.in/yaml.v2"
"github.com/codegangsta/cli" "github.com/codegangsta/cli"
) )
...@@ -183,14 +184,6 @@ func readOptions(cx *cli.Context, config *Config) (err error) { ...@@ -183,14 +184,6 @@ func readOptions(cx *cli.Context, config *Config) (err error) {
// readConfigFile reads and parses the configuration file // readConfigFile reads and parses the configuration file
func readConfigFile(filename string, config *Config) error { func readConfigFile(filename string, config *Config) error {
ext := filepath.Ext(filename)
formatYAML := true
switch ext {
case "json":
formatYAML = false
}
// step: read in the contents of the file // step: read in the contents of the file
content, err := ioutil.ReadFile(filename) content, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
...@@ -198,13 +191,11 @@ func readConfigFile(filename string, config *Config) error { ...@@ -198,13 +191,11 @@ func readConfigFile(filename string, config *Config) error {
} }
// step: attempt to un-marshal the data // step: attempt to un-marshal the data
switch formatYAML { if isJson := filepath.Ext(filename) == "json"; isJson {
case false:
err = json.Unmarshal(content, config) err = json.Unmarshal(content, config)
default: } else {
err = yaml.Unmarshal(content, config) err = yaml.Unmarshal(content, config)
} }
if err != nil { if err != nil {
return err return err
} }
......
...@@ -21,30 +21,28 @@ import ( ...@@ -21,30 +21,28 @@ import (
"net/url" "net/url"
"time" "time"
"github.com/gambol99/go-oidc/jose"
"github.com/gambol99/go-oidc/oidc" "github.com/gambol99/go-oidc/oidc"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
const ( const (
prog = "keycloak-proxy" prog = "keycloak-proxy"
version = "v0.0.4" version = "v0.0.5"
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"
headerUpgrade = "Upgrade" headerUpgrade = "Upgrade"
sessionCookieName = "keycloak-access" sessionCookieName = "kc-access"
sessionStateCookieName = "keycloak-state" sessionStateCookieName = "kc-state"
userContextName = "identity" userContextName = "identity"
authorizationHeader = "Authorization"
// the urls // the urls
oauthURL = "/oauth" oauthURL = "/oauth"
authorizationURL = oauthURL + "/authorize" authorizationURL = oauthURL + "/authorize"
callbackURL = oauthURL + "/callback" callbackURL = oauthURL + "/callback"
healthURL = oauthURL + "/health" healthURL = oauthURL + "/health"
signInPageURL = oauthURL + "/sign_in"
accessForbiddenPageURL = oauthURL + "/forbidden"
) )
var ( var (
...@@ -140,23 +138,3 @@ type sessionState struct { ...@@ -140,23 +138,3 @@ type sessionState struct {
// the refresh token if any // the refresh token if any
refreshToken string refreshToken string
} }
// userContext represents a user
type userContext struct {
// the id of the user
id string
// the email associated to the user
email string
// a name of the user
name string
// the preferred name
preferredName string
// the expiration of the access token
expiresAt time.Time
// a set of roles associated
roles []string
// the access token itself
token jose.JWT
// the claims associated to the token
claims jose.Claims
}
...@@ -22,6 +22,7 @@ import ( ...@@ -22,6 +22,7 @@ import (
"time" "time"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/gambol99/go-oidc/jose"
"github.com/gambol99/go-oidc/oauth2" "github.com/gambol99/go-oidc/oauth2"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
...@@ -29,8 +30,9 @@ import ( ...@@ -29,8 +30,9 @@ import (
// //
// The logic is broken into four handlers just to simplify the code // The logic is broken into four handlers just to simplify the code
// //
// a) authenticationHandler checks for a session cookie and if doesn't exists redirects to AS, verifies the token is valid and if required refreshes the token // a) entrypointHandler checks if the the uri requires authentication
// b) admissionHandler verifies the access token has access to the resource // b) authenticationHandler verifies the access token
// c) admissionHandler verifies that the token is authorized to access to uri resource
// c) proxyHandler is responsible for handling the reverse proxy to the upstream endpoint // c) proxyHandler is responsible for handling the reverse proxy to the upstream endpoint
// //
...@@ -62,8 +64,7 @@ func (r *KeycloakProxy) entrypointHandler() gin.HandlerFunc { ...@@ -62,8 +64,7 @@ func (r *KeycloakProxy) entrypointHandler() gin.HandlerFunc {
return return
} }
// step: check if authentication is required - gin doesn't support wildcard // step: check if authentication is required - gin doesn't support wildcard url, so we have have to use prefixes
// url, so we have have to use prefixes
for _, resource := range r.config.Resources { for _, resource := range r.config.Resources {
if strings.HasPrefix(cx.Request.RequestURI, resource.URL) { if strings.HasPrefix(cx.Request.RequestURI, resource.URL) {
if containedIn(cx.Request.Method, resource.Methods) { if containedIn(cx.Request.Method, resource.Methods) {
...@@ -81,24 +82,21 @@ func (r *KeycloakProxy) entrypointHandler() gin.HandlerFunc { ...@@ -81,24 +82,21 @@ func (r *KeycloakProxy) entrypointHandler() gin.HandlerFunc {
// authenticationHandler is responsible for verifying the access token // authenticationHandler is responsible for verifying the access token
func (r *KeycloakProxy) authenticationHandler() gin.HandlerFunc { func (r *KeycloakProxy) authenticationHandler() gin.HandlerFunc {
return func(cx *gin.Context) { return func(cx *gin.Context) {
// step: is authentication required on this var session jose.JWT
// step: is authentication required on this uri?
if _, found := cx.Get(authRequired); !found { if _, found := cx.Get(authRequired); !found {
return return
} }
// step: extract the token if there is one // step: retrieve the access token from the request
// a) if there is no token, we check for session state and if so, we try to refresh the token session, isBearer, err := r.getSessionToken(cx)
// b) there is no token or session state, we simple redirect to keycloak
session, err := r.getSessionToken(cx)
if err != nil { if err != nil {
// step: there isn't a session cookie, do we have refresh session cookie? // step: there isn't a session cookie, do we have refresh session cookie?
if err == ErrSessionNotFound && r.config.RefreshSession { if err == ErrSessionNotFound && r.config.RefreshSession && !isBearer {
session, err = r.refreshUserSessionToken(cx) session, err = r.refreshUserSessionToken(cx)
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to refresh the access token")
"error": err.Error(),
}).Errorf("failed to refresh the access token")
r.redirectToAuthorization(cx) r.redirectToAuthorization(cx)
return return
} }
...@@ -112,13 +110,13 @@ func (r *KeycloakProxy) authenticationHandler() gin.HandlerFunc { ...@@ -112,13 +110,13 @@ func (r *KeycloakProxy) authenticationHandler() gin.HandlerFunc {
// step: retrieve the identity and inject in the context // step: retrieve the identity and inject in the context
userContext, err := r.getUserContext(session) userContext, err := r.getUserContext(session)
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to retrieve the identity from the token")
"error": err.Error(),
}).Errorf("failed to retrieve the identity from the token")
r.redirectToAuthorization(cx) r.redirectToAuthorization(cx)
return return
} }
userContext.bearerToken = isBearer
cx.Set(userContextName, userContext) cx.Set(userContextName, userContext)
// step: verify the access token // step: verify the access token
...@@ -126,18 +124,25 @@ func (r *KeycloakProxy) authenticationHandler() gin.HandlerFunc { ...@@ -126,18 +124,25 @@ func (r *KeycloakProxy) authenticationHandler() gin.HandlerFunc {
// step: if the error post verification is anything other than a token expired error // 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 // we immediately throw an access forbidden - as there is something messed up in the token
if err != ErrAccessTokenExpired { if err != ErrAccessTokenExpired {
log.WithFields(log.Fields{ log.WithFields(log.Fields{"error": err.Error()}).Errorf("invalid access token")
"token": userContext.token,
"error": err.Error(),
}).Errorf("invalid access token")
r.accessForbidden(cx) r.accessForbidden(cx)
return return
} }
if isBearer {
log.WithFields(log.Fields{
"username": userContext.name,
"expired_on" : userContext.expiresAt.String(),
}).Errorf("the session has expired and we are using bearer token")
r.redirectToAuthorization(cx)
return
}
// step: are we refreshing the access tokens? // step: are we refreshing the access tokens?
if !r.config.RefreshSession { if !r.config.RefreshSession {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"username": userContext.name, "username": userContext.name,
"expired_on" : userContext.expiresAt.String(),
}).Errorf("the session has expired and token refreshing is disabled") }).Errorf("the session has expired and token refreshing is disabled")
r.redirectToAuthorization(cx) r.redirectToAuthorization(cx)
return return
...@@ -145,9 +150,7 @@ func (r *KeycloakProxy) authenticationHandler() gin.HandlerFunc { ...@@ -145,9 +150,7 @@ func (r *KeycloakProxy) authenticationHandler() gin.HandlerFunc {
// step: attempt to refresh the access token // step: attempt to refresh the access token
if _, err := r.refreshUserSessionToken(cx); err != nil { if _, err := r.refreshUserSessionToken(cx); err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to refresh the access token")
"error": err.Error(),
}).Errorf("failed to refresh the access token")
r.redirectToAuthorization(cx) r.redirectToAuthorization(cx)
return return
} }
...@@ -232,7 +235,6 @@ func (r *KeycloakProxy) admissionHandler() gin.HandlerFunc { ...@@ -232,7 +235,6 @@ func (r *KeycloakProxy) admissionHandler() gin.HandlerFunc {
return return
} }
} }
log.WithFields(log.Fields{ log.WithFields(log.Fields{
...@@ -240,6 +242,7 @@ func (r *KeycloakProxy) admissionHandler() gin.HandlerFunc { ...@@ -240,6 +242,7 @@ func (r *KeycloakProxy) admissionHandler() gin.HandlerFunc {
"username" : identity.name, "username" : identity.name,
"resource" : resource.URL, "resource" : resource.URL,
"expires" : identity.expiresAt.Sub(time.Now()), "expires" : identity.expiresAt.Sub(time.Now()),
"bearer" : identity.bearerToken,
}).Debugf("resource access permitted: %s", cx.Request.RequestURI) }).Debugf("resource access permitted: %s", cx.Request.RequestURI)
return return
...@@ -271,9 +274,7 @@ func (r *KeycloakProxy) proxyHandler() gin.HandlerFunc { ...@@ -271,9 +274,7 @@ func (r *KeycloakProxy) proxyHandler() gin.HandlerFunc {
if isUpgradedConnection(cx.Request) { if isUpgradedConnection(cx.Request) {
log.Debugf("upgrading the connnection to %s", cx.Request.Header.Get(headerUpgrade)) log.Debugf("upgrading the connnection to %s", cx.Request.Header.Get(headerUpgrade))
if err := r.tryUpdateConnection(cx); err != nil { if err := r.tryUpdateConnection(cx); err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to upgrade the connection")
"error": err.Error(),
}).Errorf("failed to upgrade the connection")
cx.AbortWithStatus(http.StatusInternalServerError) cx.AbortWithStatus(http.StatusInternalServerError)
return return
......
...@@ -70,20 +70,35 @@ func (r *KeycloakProxy) refreshUserSessionToken(cx *gin.Context) (jose.JWT, erro ...@@ -70,20 +70,35 @@ func (r *KeycloakProxy) refreshUserSessionToken(cx *gin.Context) (jose.JWT, erro
} }
// 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
func (r *KeycloakProxy) getSessionToken(cx *gin.Context) (jose.JWT, error) { // The token can come either a session cookie or a Bearer header
func (r *KeycloakProxy) getSessionToken(cx *gin.Context) (jose.JWT, bool, error) {
var session string
isBearer := false
// step: look for a authorization header
if authHeader := cx.Request.Header.Get(authorizationHeader); authHeader != "" {
isBearer = true
items := strings.Split(authHeader, " ")
if len(items) != 2 {
return jose.JWT{}, isBearer, fmt.Errorf("invalid authorizarion header")
}
session = items[1]
} else {
// step: find the authentication cookie from the request // step: find the authentication cookie from the request
cookie := findCookie(sessionCookieName, cx.Request.Cookies()) cookie := findCookie(sessionCookieName, cx.Request.Cookies())
if cookie == nil { if cookie == nil {
return jose.JWT{}, ErrSessionNotFound return jose.JWT{}, isBearer, ErrSessionNotFound
}
session = cookie.Value
} }
// step: parse the token // step: parse the token
jwt, err := jose.ParseJWT(cookie.Value) jwt, err := jose.ParseJWT(session)
if err != nil { if err != nil {
return jose.JWT{}, err return jose.JWT{}, isBearer, err
} }
return jwt, nil return jwt, isBearer, nil
} }
// getSessionState retrieves the session state from the request // getSessionState retrieves the session state from the request
...@@ -105,18 +120,23 @@ func (r *KeycloakProxy) getUserContext(token jose.JWT) (*userContext, error) { ...@@ -105,18 +120,23 @@ func (r *KeycloakProxy) getUserContext(token jose.JWT) (*userContext, error) {
return nil, err return nil, err
} }
// step: get the preferred name
preferredName, _, err := claims.StringClaim("preferred_username")
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Warningf("unable to extract the preferred name from the token claims")
}
// step: extract the identity // step: extract the identity
ident, err := oidc.IdentityFromClaims(claims) identity, err := oidc.IdentityFromClaims(claims)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// step: ensure we have and can extract the preferred name of the user, if not, we set to the ID
preferredName, found, err := claims.StringClaim("preferred_username")
if err != nil || !found {
log.WithFields(log.Fields{
"id": identity.ID,
"email": identity.Email,
}).Warnf("the token does not container a preferred_username")
preferredName = identity.ID
}
var list []string var list []string
// step: extract the roles from the access token // step: extract the roles from the access token
...@@ -132,11 +152,11 @@ func (r *KeycloakProxy) getUserContext(token jose.JWT) (*userContext, error) { ...@@ -132,11 +152,11 @@ func (r *KeycloakProxy) getUserContext(token jose.JWT) (*userContext, error) {
} }
return &userContext{ return &userContext{
id: ident.ID, id: identity.ID,
name: preferredName, name: preferredName,
preferredName: preferredName, preferredName: preferredName,
email: ident.Email, email: identity.Email,
expiresAt: ident.ExpiresAt, expiresAt: identity.ExpiresAt,
roles: list, roles: list,
token: token, token: token,
claims: claims, claims: claims,
......
/*
Copyright 2015 All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"time"
"github.com/gambol99/go-oidc/jose"
)
// userContext represents a user
type userContext struct {
// the id of the user
id string
// the email associated to the user
email string
// a name of the user
name string
// the preferred name
preferredName string
// the expiration of the access token
expiresAt time.Time
// a set of roles associated
roles []string
// the access token itself
token jose.JWT
// the claims associated to the token
claims jose.Claims
// whether the context is from a session cookie or authorization header
bearerToken bool
}
func (r userContext) isBearerToken() bool {
return r.bearerToken
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment