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

Merge pull request #38 from gambol99/expiration

additional endpoints
parents f30f296e 5d43079b
No related branches found
No related tags found
No related merge requests found
...@@ -266,3 +266,12 @@ Or on the command line ...@@ -266,3 +266,12 @@ Or on the command line
The proxy support enforcing mutual TLS for the clients by simply adding the --tls-ca-certificate command line option or config file option. All clients connecting must present a ceritificate The proxy support enforcing mutual TLS for the clients by simply adding the --tls-ca-certificate command line option or config file option. All clients connecting must present a ceritificate
which was signed by the CA being used. which was signed by the CA being used.
#### **Endpoints**
* **/oauth/authorize** is authentication endpoint which will generate the openid redirect to the provider
* **/oauth/callback** is provider openid callback endpoint
* **/oauth/expired** is a helper endpoint to check if a access token has expired, 200 for ok and, 401 for no token and 401 for expired
* **/oauth/token** is a helper endpoint which will display the current access token for you
* **/oauth/health** is the health checking endpoint for the proxy
...@@ -16,6 +16,8 @@ max_session: 1h ...@@ -16,6 +16,8 @@ max_session: 1h
log_requests: true log_requests: true
# log in json format # log in json format
log_json_format: true log_json_format: true
# do not redirec the request, simple 307 it
no-redirects: false
# the location of a certificate you wish the proxy to use for TLS support # the location of a certificate you wish the proxy to use for TLS support
tls_cert: tls_cert:
# the location of a private key for TLS # the location of a private key for TLS
......
...@@ -38,6 +38,8 @@ const ( ...@@ -38,6 +38,8 @@ const (
authorizationURL = oauthURL + "/authorize" authorizationURL = oauthURL + "/authorize"
callbackURL = oauthURL + "/callback" callbackURL = oauthURL + "/callback"
healthURL = oauthURL + "/health" healthURL = oauthURL + "/health"
tokenURL = oauthURL + "/token"
expiredURL = oauthURL + "/expired"
) )
var ( var (
......
...@@ -16,6 +16,7 @@ limitations under the License. ...@@ -16,6 +16,7 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"net/http" "net/http"
"path" "path"
"regexp" "regexp"
...@@ -25,6 +26,7 @@ import ( ...@@ -25,6 +26,7 @@ import (
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/coreos/go-oidc/jose" "github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/oauth2" "github.com/coreos/go-oidc/oauth2"
"github.com/coreos/go-oidc/oidc"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/unrolled/secure" "github.com/unrolled/secure"
) )
...@@ -569,6 +571,62 @@ func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) { ...@@ -569,6 +571,62 @@ func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) {
r.redirectToURL(state, cx) r.redirectToURL(state, cx)
} }
//
// expirationHandler checks if the token has expired
//
func (r *KeycloakProxy) expirationHandler(cx *gin.Context) {
// step: get the access token from the request
token, err := r.getSession(cx)
if err != nil {
if err == ErrSessionNotFound {
cx.AbortWithError(http.StatusUnauthorized, err)
return
}
cx.AbortWithError(http.StatusInternalServerError, err)
return
}
// step: decode the claims from the tokens
claims, err := token.Claims()
if err != nil {
cx.AbortWithError(http.StatusInternalServerError, fmt.Errorf("unable to extract the claims"))
return
}
// step: extract the identity
identity, err := oidc.IdentityFromClaims(claims)
if err != nil {
cx.AbortWithError(http.StatusInternalServerError, fmt.Errorf("unable to extract identity"))
return
}
// step: check if token expired
if time.Now().After(identity.ExpiresAt) {
cx.AbortWithStatus(http.StatusForbidden)
} else {
cx.AbortWithStatus(http.StatusOK)
}
}
//
// tokenHandle display access token to screen
//
func (r *KeycloakProxy) tokenHandler(cx *gin.Context) {
// step: extract the access token from the request
token, err := r.getSession(cx)
if err != nil {
if err == ErrSessionNotFound {
cx.AbortWithError(http.StatusOK, err)
return
}
cx.AbortWithError(http.StatusInternalServerError, fmt.Errorf("unable to retrieve session, error: %s", err))
return
}
// step: write the json content
cx.Writer.Header().Set("Content-Type", "application/json")
cx.String(http.StatusOK, fmt.Sprintf("%s", token.Payload))
}
// //
// healthHandler is a health check handler for the service // healthHandler is a health check handler for the service
// //
......
...@@ -16,9 +16,11 @@ limitations under the License. ...@@ -16,9 +16,11 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"net/http" "net/http"
"strings" "strings"
"testing" "testing"
"time"
"github.com/coreos/go-oidc/jose" "github.com/coreos/go-oidc/jose"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
...@@ -382,6 +384,73 @@ func TestSecurityHandler(t *testing.T) { ...@@ -382,6 +384,73 @@ func TestSecurityHandler(t *testing.T) {
} }
} }
func newFakeJWTToken(t *testing.T, claims jose.Claims) *jose.JWT {
token, err := jose.NewJWT(
jose.JOSEHeader{"alg": "RS256"}, claims,
)
if err != nil {
t.Fatalf("failed to create the jwt token, error: %s", err)
}
return &token
}
func TestExpirationHandler(t *testing.T) {
proxy := newFakeKeycloakProxy(t)
cases := []struct {
Token *jose.JWT
HTTPCode int
}{
{
HTTPCode: http.StatusUnauthorized,
},
{
Token: newFakeJWTToken(t, jose.Claims{
"exp": float64(time.Now().Add(-24 * time.Hour).Unix()),
}),
HTTPCode: http.StatusInternalServerError,
},
{
Token: newFakeJWTToken(t, jose.Claims{
"exp": float64(time.Now().Add(10 * time.Hour).Unix()),
"iss": "https://keycloak.example.com/auth/realms/commons",
"sub": "1e11e539-8256-4b3b-bda8-cc0d56cddb48",
"email": "gambol99@gmail.com",
"name": "Rohith Jayawardene",
"preferred_username": "rjayawardene",
}),
HTTPCode: http.StatusOK,
},
{
Token: newFakeJWTToken(t, jose.Claims{
"exp": float64(time.Now().Add(-24 * time.Hour).Unix()),
"iss": "https://keycloak.example.com/auth/realms/commons",
"sub": "1e11e539-8256-4b3b-bda8-cc0d56cddb48",
"email": "gambol99@gmail.com",
"name": "Rohith Jayawardene",
"preferred_username": "rjayawardene",
}),
HTTPCode: http.StatusForbidden,
},
}
for i, c := range cases {
// step: inject a resource
cx := newFakeGinContext("GET", "/oauth/expiration")
// step: add the token is there is one
if c.Token != nil {
cx.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.Token.Encode()))
}
// step: if closure so we need to get the handler each time
proxy.expirationHandler(cx)
// step: check the content result
if cx.Writer.Status() != c.HTTPCode {
t.Errorf("test case %d should have recieved: %d, but got %d", i, c.HTTPCode, cx.Writer.Status())
}
}
}
func TestHealthHandler(t *testing.T) { func TestHealthHandler(t *testing.T) {
proxy := newFakeKeycloakProxy(t) proxy := newFakeKeycloakProxy(t)
context := newFakeGinContext("GET", healthURL) context := newFakeGinContext("GET", healthURL)
......
...@@ -133,6 +133,9 @@ func (r KeycloakProxy) initializeRouter() { ...@@ -133,6 +133,9 @@ func (r KeycloakProxy) initializeRouter() {
r.router.GET(authorizationURL, r.oauthAuthorizationHandler) r.router.GET(authorizationURL, r.oauthAuthorizationHandler)
r.router.GET(callbackURL, r.oauthCallbackHandler) r.router.GET(callbackURL, r.oauthCallbackHandler)
r.router.GET(healthURL, r.healthHandler) r.router.GET(healthURL, r.healthHandler)
r.router.GET(tokenURL, r.tokenHandler)
r.router.GET(expiredURL, r.expirationHandler)
r.router.Use(r.entryPointHandler(), r.authenticationHandler(), r.admissionHandler()) r.router.Use(r.entryPointHandler(), r.authenticationHandler(), r.admissionHandler())
} }
......
...@@ -17,6 +17,7 @@ package main ...@@ -17,6 +17,7 @@ package main
import ( import (
"bufio" "bufio"
"bytes"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
...@@ -194,6 +195,7 @@ func newFakeResponse() *fakeResponse { ...@@ -194,6 +195,7 @@ func newFakeResponse() *fakeResponse {
func newFakeGinContext(method, uri string) *gin.Context { func newFakeGinContext(method, uri string) *gin.Context {
return &gin.Context{ return &gin.Context{
Request: &http.Request{ Request: &http.Request{
Method: method, Method: method,
Host: "127.0.0.1", Host: "127.0.0.1",
...@@ -218,6 +220,7 @@ type fakeResponse struct { ...@@ -218,6 +220,7 @@ type fakeResponse struct {
size int size int
status int status int
headers http.Header headers http.Header
body bytes.Buffer
} }
func (r *fakeResponse) Flush() {} func (r *fakeResponse) Flush() {}
......
...@@ -117,6 +117,13 @@ func (r *KeycloakProxy) getSessionToken(cx *gin.Context) (jose.JWT, bool, error) ...@@ -117,6 +117,13 @@ func (r *KeycloakProxy) getSessionToken(cx *gin.Context) (jose.JWT, bool, error)
return jwt, isBearer, nil return jwt, isBearer, nil
} }
// getSession is a helper methods for those whom dont care if it's a bearer token
func (r *KeycloakProxy) getSession(cx *gin.Context) (jose.JWT, error) {
token, _, err := r.getSessionToken(cx)
return token, err
}
// getSessionState retrieves the session state from the request // getSessionState retrieves the session state from the request
func (r *KeycloakProxy) getSessionState(cx *gin.Context) (*sessionState, error) { func (r *KeycloakProxy) getSessionState(cx *gin.Context) (*sessionState, error) {
// step: find the session data cookie // step: find the session data cookie
...@@ -193,7 +200,7 @@ func (r *KeycloakProxy) getUserContext(token jose.JWT) (*userContext, error) { ...@@ -193,7 +200,7 @@ func (r *KeycloakProxy) getUserContext(token jose.JWT) (*userContext, error) {
// createSession creates a session cookie with the access token // createSession creates a session cookie with the access token
func (r *KeycloakProxy) createSession(token jose.JWT, expires time.Time, cx *gin.Context) error { func (r *KeycloakProxy) createSession(token jose.JWT, expires time.Time, cx *gin.Context) error {
http.SetCookie(cx.Writer, createSessionCookie(token.Encode(), cx.Request.Host, expires)) http.SetCookie(cx.Writer, createSessionCookie(token.Encode(), cx.Request.Host, expires.Add(time.Duration(5)*time.Minute)))
return nil return nil
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment