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

- fixed up the default config values from cli flags

- fixed up the access vs id token from providers, will default to using the id token, but will attempt to use the access token
- fixed up additional issues
- updated the README to reflect the changes
- added a google openid example to the readme
- added a fix to the discovery url to remove the .well_known suffix
parent 49ab4278
No related branches found
No related tags found
No related merge requests found
......@@ -4,7 +4,17 @@
### **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.
> - Supports role based uri controls
> - Websocket connection upgrading
> - Token claim matching for additional ACL controls
> - Stateless offline refresh tokens with optional predefined session limits
> - TLS and mutual TLS support
> - JSON field bases access logs
> - Custom Sign-in and access forbidden pages
- --------
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. Although technically the proxy has no dependency on Keycloak itself. And would quite happl
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.
......@@ -96,19 +106,18 @@ resources:
methods:
- GET
# a list of roles the user must have in order to accces urls under the above
roles_allowed:
roles:
- openvpn:vpn-user
- openvpn:prod-vpn
- test
- url: /admin
methods:
- GET
roles_allowed:
roles:
- openvpn:vpn-user
- openvpn:commons-prod-vpn
```
#### **Example Usage**
Assuming you have some web service you wish protected by Keycloak;
......@@ -158,6 +167,29 @@ bin/keycloak-proxy \
--resource="uri=/backend|roles=test1"
```
#### **Google OpenID**
Technically though the role extensions do require a Keycloak IDP or at the very least a IDP that produces a token which contains roles scopes, there's nothing stopping you from using it against any OpenID providers, such as Google. Go to the Google Developers Console and create a new application *(via "Enable and Manage APIs -> Credentials)*. Once you've created the application, take the client id, secret and make sure you've added the callback url to the application scope *(using the default this would be http://127.0.0.1:3000/oauth/callback)*
``` shell
bin/keycloak-proxy \
--discovery-url=https://accounts.google.com/.well-known/openid-confuration \
--client-id=<CLIENT_ID> \
--secret=<CLIENT_SECRET> \
--resource="uri=/" \
--verbose=true
```
Open a browser an go to http://127.0.0.1:3000 and you should be redirected to Google for authenticate and back the application when done and you should see something like the below.
```shell
DEBU[0002] resource access permitted: / access=permitted bearer=false expires=57m51.32029042s resource=/ username=gambol99@gmail.com
2016-02-06 13:59:01.680300 I | http: proxy error: dial tcp 127.0.0.1:8081: getsockopt: connection refused
DEBU[0002] resource access permitted: /favicon.ico access=permitted bearer=false expires=57m51.144004098s resource=/ username=gambol99@gmail.com
2016-02-06 13:59:01.856716 I | http: proxy error: dial tcp 127.0.0.1:8081: getsockopt: connection refused
```
#### **Upstream Headers**
On protected resources the upstream endpoint will receive a number of headers added by the proxy;
......
......@@ -33,6 +33,10 @@ import (
// newDefaultConfig returns a initialized config
func newDefaultConfig() *Config {
return &Config{
Listen: "127.0.0.1:3000",
RedirectionURL: "http://127.0.0.1:3000",
Upstream: "http://127.0.0.1:8081",
MaxSession: time.Duration(1) * time.Hour,
TagData: make(map[string]string, 0),
ClaimsMatch: make(map[string]string, 0),
}
......@@ -241,6 +245,8 @@ func readConfigFile(filename string, config *Config) error {
// getOptions returns the command line options
func getOptions() []cli.Flag {
defaults := newDefaultConfig()
return []cli.Flag{
cli.StringFlag{
Name: "config",
......@@ -249,7 +255,7 @@ func getOptions() []cli.Flag {
cli.StringFlag{
Name: "listen",
Usage: "the interface the service should be listening on",
Value: "127.0.0.1:8080",
Value: defaults.Listen,
},
cli.StringFlag{
Name: "secret",
......@@ -266,7 +272,7 @@ func getOptions() []cli.Flag {
cli.StringFlag{
Name: "upstream-url",
Usage: "the url for the upstream endpoint you wish to proxy to",
Value: "http://127.0.0.1:8080",
Value: defaults.Upstream,
},
cli.StringFlag{
Name: "encryption-key",
......@@ -319,7 +325,7 @@ func getOptions() []cli.Flag {
cli.DurationFlag{
Name: "max-session",
Usage: "if refresh sessions are enabled we can limit their duration via this",
Value: time.Duration(1) * time.Hour,
Value: defaults.MaxSession,
},
cli.BoolFlag{
Name: "skip-token-verification",
......@@ -331,7 +337,7 @@ func getOptions() []cli.Flag {
},
cli.BoolFlag{
Name: "refresh-sessions",
Usage: "enables the refreshing of tokens via offline access",
Usage: "enables the refreshing of tokens via offline access (defaults false)",
},
cli.BoolTFlag{
Name: "json-logging",
......
......@@ -453,6 +453,7 @@ func (r *KeycloakProxy) oauthAuthorizationHandler(cx *gin.Context) {
//
// oauthCallbackHandler is responsible for handling the response from keycloak
//
// @@TODO need to clean up this method somewhat
func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) {
// step: is token verification switched on?
if r.config.SkipTokenVerification {
......@@ -460,15 +461,16 @@ func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) {
return
}
// step: ensure we have a authorization code
// step: ensure we have a authorization code to exchange
code := cx.Request.URL.Query().Get("code")
if code == "" {
log.Error("failed to get the code callback request")
log.WithFields(log.Fields{"client_ip": cx.ClientIP()}).Error("code parameter not found in callback request")
r.accessForbidden(cx)
return
}
// step: grab the state from request
// step: grab the state from request, otherwise default to root url
state := cx.Request.URL.Query().Get("state")
if state == "" {
state = "/"
......@@ -477,19 +479,33 @@ func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) {
// step: exchange the authorization for a access token
response, err := r.getToken(oauth2.GrantTypeAuthCode, code)
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to retrieve access token from authentication service")
log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to exchange code for access token")
r.accessForbidden(cx)
return
}
// step: decode and parse the access token
token, identity, err := r.parseToken(response.AccessToken)
// step: decode and verify the id token
token, identity, err := r.parseToken(response.IDToken)
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to parse jwt token for identity")
log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to parse id token for identity")
r.accessForbidden(cx)
return
}
if err := r.verifyToken(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?
ac, id, err := r.parseToken(response.AccessToken)
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to parse the access token, using id token only")
} else {
token = ac
identity = id
}
log.WithFields(log.Fields{
"email": identity.Email,
"expires": identity.ExpiresAt,
......
......@@ -48,6 +48,7 @@ func main() {
if err := readOptions(cx, config); err != nil {
printUsage(err.Error())
}
// step: validate the configuration
if err := config.isValid(); err != nil {
printUsage(err.Error())
......@@ -55,13 +56,11 @@ func main() {
// step: create the proxy
proxy, err := newKeycloakProxy(config)
if err != nil {
fmt.Fprintf(os.Stderr, "[error] %s", err)
os.Exit(1)
printUsage(err.Error())
}
// step: start the service
if err := proxy.Run(); err != nil {
fmt.Fprintf(os.Stderr, "[error] %s", err)
os.Exit(1)
printUsage(err.Error())
}
// step: setup the termination signals
signalChannel := make(chan os.Signal)
......@@ -75,6 +74,6 @@ func main() {
// printUsage display the command line usage and error
func printUsage(message string) {
fmt.Fprintf(os.Stderr, "[error] %s\n", message)
fmt.Fprintf(os.Stderr, "\n[error] %s\n", message)
os.Exit(1)
}
......@@ -35,6 +35,9 @@ func TestIsValid(t *testing.T) {
{
Resource: &Resource{},
},
{
Resource: &Resource{URL: "/oauth"},
},
{
Resource: &Resource{
URL: "/test",
......
......@@ -144,12 +144,8 @@ func (r *KeycloakProxy) getUserContext(token jose.JWT) (*userContext, error) {
// 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(claimPreferredName)
if err != nil || !found {
log.WithFields(log.Fields{
"id": identity.ID,
"email": identity.Email,
}).Warnf("the token does not container a %s", claimPreferredName)
// choice: set the preferredName to the ID if claim not found
preferredName = identity.ID
// choice: set the preferredName to the Email if claim not found
preferredName = identity.Email
}
// step: retrieve the audience from access token
......
......@@ -89,6 +89,12 @@ func initializeOpenID(discoveryURL, clientID, clientSecret, redirectURL string,
var err error
var providerConfig oidc.ProviderConfig
// step: fix up the url if required, the underlining lib will add the .well-known/openid-configuration to
// the discovery url for us.
if strings.HasSuffix(discoveryURL, "/.well-known/openid-configuration") {
discoveryURL = strings.TrimSuffix(discoveryURL, "/.well-known/openid-configuration")
}
// step: attempt to retrieve the provider configuration
gotConfig := false
for i := 0; i < 3; i++ {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment