Skip to content
This repository has been archived by the owner on Jan 20, 2023. It is now read-only.

Commit

Permalink
Support single-host mode of Che server. (#11)
Browse files Browse the repository at this point in the history
Support single-host mode of Che server.

* A custom URI prefix for the auth redirect can be configured, called 
public_base_path. This is so that we can construct valid externally 
reachable URLs even behind a path-rewriting  ingress
* Change the order in which the auth token is located. First we try to
find it in the query params, then in the Authorization header as a bearer
token and only then in the cookie. This enables us to "refresh" the token
from the client side easily.
* On any error to validate the token (apart from the inability to parse the
token in the first place) we know send the auth redirect instead of an
error. This should help the client side refresh the token on timeouts, etc.
* Do not set the cookie in the response if cookies are not enabled in the
config.
* Respond with 403 - Forbidden if cookies are not enabled. In this case
the client needs to directly authenticate with the backend server.

Signed-off-by: Lukas Krejci <lkrejci@redhat.com>
  • Loading branch information
metlos authored Sep 4, 2019
1 parent 1f87a88 commit 71013a4
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 35 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,15 @@ jwtproxy:
nonce_storage:
type: <string|nil>
options: <map[string]interface{}>

# a path on which the `access_token` cookie should be stored. This means that the browsers will only use the
# cookie on requests for URLs on the sub paths of the cookie_path.
cookie_path: <string|nil>

# In case the JWT proxy is placed behind a path-rewriting reverse proxy (such as Kubernetes Ingress)
# the value of this property can be used to modify the "apparent" path of the request as the JWT proxy
# sees it when composing the redirect to be used after the authentication request.
public_base_path: <string|nil>
```
#### Key Registry Key Server
Expand Down
6 changes: 4 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
"os"
"time"

"gopkg.in/yaml.v2"
yaml "gopkg.in/yaml.v2"
)

// URL is a custom URL type that allows validation at configuration load time.
Expand Down Expand Up @@ -113,17 +113,19 @@ type SignerProxyConfig struct {
}

type VerifierConfig struct {
Upstream URL `yaml:"upstream"`
Upstream URL `yaml:"upstream"`
// Changed to string to be more JWT spec compliant - it can be either string or URL
Audience string `yaml:"audience"`
CookiesEnabled bool `yaml:"auth_cookies_enabled"`
CookiePath string `yaml:"cookie_path"`
AuthRedirect string `yaml:"auth_redirect_url"`
MaxSkew time.Duration `yaml:"max_skew"`
MaxTTL time.Duration `yaml:"max_ttl"`
KeyServer RegistrableComponentConfig `yaml:"key_server"`
NonceStorage RegistrableComponentConfig `yaml:"nonce_storage"`
Excludes []string `yaml:"excludes"`
ClaimsVerifiers []RegistrableComponentConfig `yaml:"claims_verifiers"`
PublicBasePath string `yaml:"public_base_path"`
}

type SignerParams struct {
Expand Down
83 changes: 53 additions & 30 deletions jwt/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,37 +68,35 @@ func Sign(req *http.Request, key *key.PrivateKey, params config.SignerParams) er
return nil
}

func Verify(req *http.Request, keyServer keyserver.Reader, nonceVerifier noncestorage.NonceStorage, cookiesEnabled bool, expectedAudience string, maxSkew time.Duration, maxTTL time.Duration) (jose.Claims, error) {
func Verify(req *http.Request, keyServer keyserver.Reader, nonceVerifier noncestorage.NonceStorage, cookiesEnabled bool, expectedAudience string, maxSkew time.Duration, maxTTL time.Duration, publicBasePath string) (jose.Claims, error) {
protocol := "http"
if req.Header.Get("X-Forwarded-Proto") == "https" {
protocol = "https"
}
var token = ""
// Extract token from cookie if enabled.
if cookiesEnabled {
cookieExtractor := oidc.CookieTokenExtractor("access_token")
cookieToken, err := cookieExtractor(req)
if err == nil {
token = cookieToken
}
}

// First, try to find the token in the query params
var token = req.URL.Query().Get("token")

// Try to extract the token from the header
if token == "" {
// Not found in cookie, extract from header
headerToken, err := oidc.ExtractBearerToken(req)
if err == nil {
token = headerToken
}
}

if token == "" {
// Not found in cookies neither header, extract from query
token = req.URL.Query().Get("token")
// Try to extract token from cookie if enabled.
if token == "" && cookiesEnabled {
cookieExtractor := oidc.CookieTokenExtractor("access_token")
cookieToken, err := cookieExtractor(req)
if err == nil {
token = cookieToken
}
}

if token == "" {
// Not found anywhere
return nil, &authRequiredError{"No JWT found", protocol + "://" + req.Host + req.URL.String()}
return nil, constructVerificationError("No JWT found", protocol, publicBasePath, req)
}

// Parse token.
Expand All @@ -116,43 +114,43 @@ func Verify(req *http.Request, keyServer keyserver.Reader, nonceVerifier noncest
now := time.Now().UTC()
kid, exists := jwt.Header["kid"]
if !exists {
return nil, errors.New("Missing 'kid' claim")
return nil, constructVerificationError("Missing 'kid' claim", protocol, publicBasePath, req)
}
iss, exists, err := claims.StringClaim("iss")
if !exists || err != nil {
return nil, errors.New("Missing or invalid 'iss' claim")
return nil, constructVerificationError("Missing or invalid 'iss' claim", protocol, publicBasePath, req)
}
if expectedAudience != "" {
aud, _, err := claims.StringClaim("aud")
if err != nil {
return nil, errors.New("Invalid 'aud' claim")
return nil, constructVerificationError("Invalid 'aud' claim", protocol, publicBasePath, req)
}
if !verifyAudience(aud, expectedAudience) {
return nil, errors.New("Error - 'aud' claim mismatch")
return nil, constructVerificationError("Error - 'aud' claim mismatch", protocol, publicBasePath, req)
}
}

exp, exists, err := claims.TimeClaim("exp")
if !exists || err != nil {
return nil, errors.New("Missing or invalid 'exp' claim")
return nil, constructVerificationError("Missing or invalid 'exp' claim", protocol, publicBasePath, req)
}
if exp.Before(now) {
return nil, &authRequiredError{"Token is expired", protocol + "://" + req.Host + req.URL.String()}
return nil, constructVerificationError("Token is expired", protocol, publicBasePath, req)
}
nbf, exists, err := claims.TimeClaim("nbf")
if !exists || err != nil || nbf.After(now) {
return nil, errors.New("Missing or invalid 'nbf' claim")
return nil, constructVerificationError("Missing or invalid 'nbf' claim", protocol, publicBasePath, req)
}
iat, exists, err := claims.TimeClaim("iat")
if !exists || err != nil || iat.Add(-maxSkew).After(now) {
return nil, errors.New("Missing or invalid 'iat' claim")
return nil, constructVerificationError("Missing or invalid 'iat' claim", protocol, publicBasePath, req)
}
if exp.Sub(iat) > maxTTL {
return nil, errors.New("Invalid 'exp' claim (too long)")
return nil, constructVerificationError("Invalid 'exp' claim (too long)", protocol, publicBasePath, req)
}
jti, exists, err := claims.StringClaim("jti")
if !exists || err != nil || !nonceVerifier.Verify(jti, exp) {
return nil, errors.New("Missing or invalid 'jti' claim")
return nil, constructVerificationError("Missing or invalid 'jti' claim", protocol, publicBasePath, req)
}

// Verify signature.
Expand All @@ -161,31 +159,56 @@ func Verify(req *http.Request, keyServer keyserver.Reader, nonceVerifier noncest
return nil, err
} else if err != nil {
log.Errorf("Could not get public key from key server: %s", err)
return nil, errors.New("Unexpected key server error")
return nil, constructVerificationError("Unexpected key server error", protocol, publicBasePath, req)
}

verifier, err := publicKey.Verifier()
if err != nil {
log.Errorf("Could not create JWT verifier for public key '%s': %s", publicKey.ID(), err)
return nil, errors.New("Unexpected verifier initialization failure")
return nil, constructVerificationError("Unexpected verifier initialization failure", protocol, publicBasePath, req)
}

if verifier.Verify(jwt.Signature, []byte(jwt.Data())) != nil {
return nil, errors.New("Invalid JWT signature")
return nil, constructVerificationError("Invalid JWT signature", protocol, publicBasePath, req)
}

return claims, nil
}

func constructVerificationError(message string, protocol string, pathPrefix string, req *http.Request) error {
url := composeRedirectURL(protocol, req, pathPrefix)
return &authRequiredError{message, url}
}

func composeRedirectURL(protocol string, req *http.Request, pathPrefix string) string {
u := *req.URL

u.Path = singleJoiningSlash(singleJoiningSlash("/", pathPrefix), u.Path)
u.Host = req.Host
u.Scheme = protocol

return u.String()
}

func verifyAudience(actual string, expected string) bool {
actualURL, actualErr := url.ParseRequestURI(actual)
expectedURL, expectedErr := url.ParseRequestURI(expected)
if actualErr == nil && expectedErr == nil {
// both are URL's
return strings.EqualFold(actualURL.Scheme+"://"+actualURL.Host, expectedURL.Scheme+"://"+expectedURL.Host)
ret := strings.EqualFold(actualURL.Scheme+"://"+actualURL.Host, expectedURL.Scheme+"://"+expectedURL.Host)
if !ret {
log.Errorf("aud verification failed. actual: %s, expected: %s", actual, expected)
}

return ret
} else if actualErr != nil && expectedErr != nil {
// both are simple strings
return actual == expected
ret := actual == expected
if !ret {
log.Errorf("aud verification failed. actual: %s, expected: %s", actual, expected)
}

return ret
} else {
// One is URL and another is not, which is not valid
return false
Expand Down
2 changes: 1 addition & 1 deletion jwt/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,6 @@ func signAndModify(t *testing.T, req *http.Request, p signAndVerifyParams, modif

func Verify(req *http.Request, p signAndVerifyParams) error {
// Verify.
_, err := jwt.Verify(req, p.services, p.services, p.cookiesEnabled, p.aud, p.maxSkew, p.maxTTL)
_, err := jwt.Verify(req, p.services, p.services, p.cookiesEnabled, p.aud, p.maxSkew, p.maxTTL, "")
return err
}
14 changes: 12 additions & 2 deletions jwt/proxy_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func NewJWTVerifierHandler(cfg config.VerifierConfig) (*StoppableProxyHandler, e

// Create a reverse proxy.Handler that will verify JWT from http.Requests.
handler := func(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
signedClaims, err := Verify(r, keyServer, nonceStorage, cfg.CookiesEnabled, cfg.Audience, cfg.MaxSkew, cfg.MaxTTL)
signedClaims, err := Verify(r, keyServer, nonceStorage, cfg.CookiesEnabled, cfg.Audience, cfg.MaxSkew, cfg.MaxTTL, cfg.PublicBasePath)
if err != nil {
if authErr, ok := err.(*authRequiredError); ok {
if redirectUrl != nil {
Expand Down Expand Up @@ -206,13 +206,23 @@ func NewAuthenticationHandler(cfg config.VerifierConfig) (*StoppableProxyHandler
resp = goproxy.NewResponse(r, goproxy.ContentTypeText, http.StatusOK, "")
resp.Header.Add("Access-Control-Allow-Headers", "authorization,origin,access-control-allow-origin,access-control-request-headers,content-type,access-control-request-method,accept")
resp.Header.Add("Access-Control-Allow-Methods", "GET")
} else if !cfg.CookiesEnabled {
return r, goproxy.NewResponse(r, goproxy.ContentTypeText, http.StatusForbidden, "Cookies are disabled for this endpoint. You need to provide the access token on your own.")
} else {
token, err := oidc.ExtractBearerToken(r)
if err != nil {
return r, goproxy.NewResponse(r, goproxy.ContentTypeText, http.StatusForbidden, "No token found in request")
}
resp = goproxy.NewResponse(r, goproxy.ContentTypeText, http.StatusNoContent, "")
cookie := http.Cookie{Name: "access_token", Value: token, HttpOnly: true, Path: "/"}

var cookiePath string
if cfg.CookiePath == "" {
cookiePath = "/"
} else {
cookiePath = cfg.CookiePath
}

cookie := http.Cookie{Name: "access_token", Value: token, HttpOnly: true, Path: cookiePath}
if redirectUrl.Scheme == "https" {
cookie.Secure = true
}
Expand Down

0 comments on commit 71013a4

Please sign in to comment.