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
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.
#### **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
log_requests: true
# log in json format
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
tls_cert:
# the location of a private key for TLS
......
......@@ -38,6 +38,8 @@ const (
authorizationURL = oauthURL + "/authorize"
callbackURL = oauthURL + "/callback"
healthURL = oauthURL + "/health"
tokenURL = oauthURL + "/token"
expiredURL = oauthURL + "/expired"
)
var (
......
......@@ -16,6 +16,7 @@ limitations under the License.
package main
import (
"fmt"
"net/http"
"path"
"regexp"
......@@ -25,6 +26,7 @@ import (
log "github.com/Sirupsen/logrus"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/oauth2"
"github.com/coreos/go-oidc/oidc"
"github.com/gin-gonic/gin"
"github.com/unrolled/secure"
)
......@@ -569,6 +571,62 @@ func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) {
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
//
......
......@@ -16,9 +16,11 @@ limitations under the License.
package main
import (
"fmt"
"net/http"
"strings"
"testing"
"time"
"github.com/coreos/go-oidc/jose"
"github.com/gin-gonic/gin"
......@@ -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) {
proxy := newFakeKeycloakProxy(t)
context := newFakeGinContext("GET", healthURL)
......
......@@ -133,6 +133,9 @@ func (r KeycloakProxy) initializeRouter() {
r.router.GET(authorizationURL, r.oauthAuthorizationHandler)
r.router.GET(callbackURL, r.oauthCallbackHandler)
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())
}
......
......@@ -17,6 +17,7 @@ package main
import (
"bufio"
"bytes"
"io/ioutil"
"net"
"net/http"
......@@ -194,6 +195,7 @@ func newFakeResponse() *fakeResponse {
func newFakeGinContext(method, uri string) *gin.Context {
return &gin.Context{
Request: &http.Request{
Method: method,
Host: "127.0.0.1",
......@@ -218,6 +220,7 @@ type fakeResponse struct {
size int
status int
headers http.Header
body bytes.Buffer
}
func (r *fakeResponse) Flush() {}
......
......@@ -117,6 +117,13 @@ func (r *KeycloakProxy) getSessionToken(cx *gin.Context) (jose.JWT, bool, error)
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
func (r *KeycloakProxy) getSessionState(cx *gin.Context) (*sessionState, error) {
// step: find the session data cookie
......@@ -193,7 +200,7 @@ func (r *KeycloakProxy) getUserContext(token jose.JWT) (*userContext, error) {
// createSession creates a session cookie with the access token
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
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment