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

Support single-host mode of Che server. #11

Merged
merged 4 commits into from
Sep 4, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 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"`
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"`
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"`
AuthErrorRedirectURIPrefix string `yaml:"auth_error_redirect_uri_prefix"`
}

type SignerParams struct {
Expand Down
68 changes: 38 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, authErrorRedirectPrefix 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, &authRequiredError{"No JWT found", protocol + "://" + req.Host + authErrorRedirectPrefix + req.URL.String()}
}

// 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, &authRequiredError{"Missing 'kid' claim", protocol + "://" + req.Host + authErrorRedirectPrefix + req.URL.String()}
}
iss, exists, err := claims.StringClaim("iss")
if !exists || err != nil {
return nil, errors.New("Missing or invalid 'iss' claim")
return nil, &authRequiredError{"Missing or invalid 'iss' claim", protocol + "://" + req.Host + authErrorRedirectPrefix + req.URL.String()}
}
if expectedAudience != "" {
aud, _, err := claims.StringClaim("aud")
if err != nil {
return nil, errors.New("Invalid 'aud' claim")
return nil, &authRequiredError{"Invalid 'aud' claim", protocol + "://" + req.Host + authErrorRedirectPrefix + req.URL.String()}
}
if !verifyAudience(aud, expectedAudience) {
return nil, errors.New("Error - 'aud' claim mismatch")
return nil, &authRequiredError{"Error - 'aud' claim mismatch", protocol + "://" + req.Host + authErrorRedirectPrefix + req.URL.String()}
}
}

exp, exists, err := claims.TimeClaim("exp")
if !exists || err != nil {
return nil, errors.New("Missing or invalid 'exp' claim")
return nil, &authRequiredError{"Missing or invalid 'exp' claim", protocol + "://" + req.Host + authErrorRedirectPrefix + req.URL.String()}
}
if exp.Before(now) {
return nil, &authRequiredError{"Token is expired", protocol + "://" + req.Host + req.URL.String()}
return nil, &authRequiredError{"Token is expired", protocol + "://" + req.Host + authErrorRedirectPrefix + req.URL.String()}
}
nbf, exists, err := claims.TimeClaim("nbf")
if !exists || err != nil || nbf.After(now) {
return nil, errors.New("Missing or invalid 'nbf' claim")
return nil, &authRequiredError{"Missing or invalid 'nbf' claim", protocol + "://" + req.Host + authErrorRedirectPrefix + req.URL.String()}
}
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, &authRequiredError{"Missing or invalid 'iat' claim", protocol + "://" + req.Host + authErrorRedirectPrefix + req.URL.String()}
}
if exp.Sub(iat) > maxTTL {
return nil, errors.New("Invalid 'exp' claim (too long)")
return nil, &authRequiredError{"Invalid 'exp' claim (too long)", protocol + "://" + req.Host + authErrorRedirectPrefix + req.URL.String()}
}
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, &authRequiredError{"Missing or invalid 'jti' claim", protocol + "://" + req.Host + authErrorRedirectPrefix + req.URL.String()}
}

// Verify signature.
Expand All @@ -161,17 +159,17 @@ 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, &authRequiredError{"Unexpected key server error", protocol + "://" + req.Host + authErrorRedirectPrefix + req.URL.String()}
}

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, &authRequiredError{"Unexpected verifier initialization failure", protocol + "://" + req.Host + authErrorRedirectPrefix + req.URL.String()}
}

if verifier.Verify(jwt.Signature, []byte(jwt.Data())) != nil {
return nil, errors.New("Invalid JWT signature")
return nil, &authRequiredError{"Invalid JWT signature", protocol + "://" + req.Host + authErrorRedirectPrefix + req.URL.String()}
}

return claims, nil
Expand All @@ -182,10 +180,20 @@ func verifyAudience(actual string, expected string) bool {
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.AuthErrorRedirectURIPrefix)
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