From 376b42a04fd3c8d220252d8172ceddcebbcae66d Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 27 Sep 2019 17:11:12 +0100 Subject: [PATCH 1/3] Gate SSO redirect on optional state whitelist --- .../console/templates/deployment.yaml | 2 ++ src/jetstream/auth.go | 21 +++++++++++++++++++ src/jetstream/default.config.properties | 3 +++ src/jetstream/main.go | 1 + .../repository/interfaces/structs.go | 1 + 5 files changed, 28 insertions(+) diff --git a/deploy/kubernetes/console/templates/deployment.yaml b/deploy/kubernetes/console/templates/deployment.yaml index 74e04b3454..1f36fc9548 100644 --- a/deploy/kubernetes/console/templates/deployment.yaml +++ b/deploy/kubernetes/console/templates/deployment.yaml @@ -247,6 +247,8 @@ spec: value: {{default "false" .Values.console.ssoLogin | quote}} - name: SSO_OPTIONS value: {{default "" .Values.console.ssoOptions | quote}} + - name: SSO_WHITELIST + value: {{ default "" .Values.console.ssoWhiteList | quote }} {{- if .Values.console.templatesConfigMapName }} - name: TEMPLATE_DIR value: /etc/templates diff --git a/src/jetstream/auth.go b/src/jetstream/auth.go index f4b1d2bf9c..eeda03cc3a 100644 --- a/src/jetstream/auth.go +++ b/src/jetstream/auth.go @@ -102,11 +102,32 @@ func (p *portalProxy) initSSOlogin(c echo.Context) error { return err } + if !safeState(state, p.Config.SSOWhiteList) { + err := interfaces.NewHTTPShadowError( + http.StatusUnauthorized, + "SSO Login: Disallowed state", + "SSO Login: Disallowed state") + return err + } + redirectURL := fmt.Sprintf("%s/oauth/authorize?response_type=code&client_id=%s&redirect_uri=%s", p.Config.ConsoleConfig.AuthorizationEndpoint, p.Config.ConsoleConfig.ConsoleClient, url.QueryEscape(getSSORedirectURI(state, state, ""))) c.Redirect(http.StatusTemporaryRedirect, redirectURL) return nil } +func safeState(state string, whiteListStr string) bool { + if len(whiteListStr) == 0 { + return true; + } + + whiteList := strings.Split(whiteListStr, ",") + if len(whiteList) == 0 { + return true; + } + + return ArrayContainsString(whiteList, state) +} + func getSSORedirectURI(base string, state string, endpointGUID string) string { baseURL, _ := url.Parse(base) baseURL.Path = "" diff --git a/src/jetstream/default.config.properties b/src/jetstream/default.config.properties index a7cb4f5a64..711f0853c5 100644 --- a/src/jetstream/default.config.properties +++ b/src/jetstream/default.config.properties @@ -20,6 +20,9 @@ ENCRYPTION_KEY=B374A26A71490437AA024E4FADD5B497FDFF1A8EA6FF12F6FB65AF2720B59CCF SQLITE_KEEP_DB=true UI_PATH=../../dist +SSO_LOGIN=false +SSO_WHITELIST= + # Enable feature in tech preview ENABLE_TECH_PREVIEW=false diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 730ccc58e9..5aa1128777 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -369,6 +369,7 @@ func showSSOConfig(portalProxy *portalProxy) { log.Infof("SSO Configuration:") log.Infof("... SSO Enabled : %t", portalProxy.Config.SSOLogin) log.Infof("... SSO Options : %s", portalProxy.Config.SSOOptions) + log.Infof("... SSO Redirect Whitelist : %s", portalProxy.Config.SSOWhiteList) } func getEncryptionKey(pc interfaces.PortalConfig) ([]byte, error) { diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index 70082958ba..b7ed395771 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -335,6 +335,7 @@ type PortalConfig struct { AutoRegisterCFName string `configName:"AUTO_REG_CF_NAME"` SSOLogin bool `configName:"SSO_LOGIN"` SSOOptions string `configName:"SSO_OPTIONS"` + SSOWhiteList string `configName:"SSO_WHITELIST"` AuthEndpointType string `configName:"AUTH_ENDPOINT_TYPE"` CookieDomain string `configName:"COOKIE_DOMAIN"` LogLevel string `configName:"LOG_LEVEL"` From 58a445edcc8b969b87bf3a60c5627da38d0c4b76 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 1 Oct 2019 09:43:56 +0100 Subject: [PATCH 2/3] Use CompareUrl for SSO_WHITELIST comparison --- src/jetstream/auth.go | 19 ++++++++++++------- src/jetstream/utils.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/jetstream/auth.go b/src/jetstream/auth.go index eeda03cc3a..b72a842a9d 100644 --- a/src/jetstream/auth.go +++ b/src/jetstream/auth.go @@ -97,16 +97,16 @@ func (p *portalProxy) initSSOlogin(c echo.Context) error { if len(state) == 0 { err := interfaces.NewHTTPShadowError( http.StatusUnauthorized, - "SSO Login: State parameter missing", - "SSO Login: State parameter missing") + "SSO Login: Redirect state parameter missing", + "SSO Login: Redirect state parameter missing") return err } - if !safeState(state, p.Config.SSOWhiteList) { + if !safeSSORedirectState(state, p.Config.SSOWhiteList) { err := interfaces.NewHTTPShadowError( http.StatusUnauthorized, - "SSO Login: Disallowed state", - "SSO Login: Disallowed state") + "SSO Login: Disallowed redirect state", + "SSO Login: Disallowed redirect state") return err } @@ -115,7 +115,7 @@ func (p *portalProxy) initSSOlogin(c echo.Context) error { return nil } -func safeState(state string, whiteListStr string) bool { +func safeSSORedirectState(state string, whiteListStr string) bool { if len(whiteListStr) == 0 { return true; } @@ -125,7 +125,12 @@ func safeState(state string, whiteListStr string) bool { return true; } - return ArrayContainsString(whiteList, state) + for _, n := range whiteList { + if CompareURL(state, n) { + return true + } + } + return false } func getSSORedirectURI(base string, state string, endpointGUID string) string { diff --git a/src/jetstream/utils.go b/src/jetstream/utils.go index 5b7bc26b61..6d5ef4ab3a 100644 --- a/src/jetstream/utils.go +++ b/src/jetstream/utils.go @@ -3,6 +3,7 @@ package main import ( "strings" "unicode" + "net/url" ) // ArrayContainsString checks the string array to see if it contains the specifed value @@ -24,3 +25,37 @@ func RemoveSpaces(str string) string { return r }, str) } + +// CompareURL compares two URLs, taking into account default HTTP/HTTPS ports and ignoring query string +func CompareURL(a, b string) bool { + + ua, err := url.Parse(a) + if err != nil { + return false + } + + ub, err := url.Parse(b) + if err != nil { + return false + } + + aPort := getPort(ua) + bPort := getPort(ub) + return ua.Scheme == ub.Scheme && ua.Hostname() == ub.Hostname() && aPort == bPort && ua.Path == ub.Path +} + +func getPort(u *url.URL) string { + port := u.Port() + if len(port) == 0 { + switch u.Scheme { + case "http": + port = "80" + case "https": + port = "443" + default: + port = "" + } + } + + return port +} From 2226cc26ea5522fc7ea33ddea7b85862725b8e79 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 1 Oct 2019 13:13:28 +0100 Subject: [PATCH 3/3] Add docs --- docs/sso.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/sso.md b/docs/sso.md index 599e78f3cb..e1ae6d9b60 100644 --- a/docs/sso.md +++ b/docs/sso.md @@ -50,8 +50,16 @@ The `authorized_grant_types` value should contain `authorization_code`. If not u uaac client update cf --authorized_grant_types authorization_code ``` +## Adding a Stratos SSO State Whitelist +When SSO has been configured Stratos's log in request will contain a URL that tells SSO where to return to. When using a browser this is automatically populated. To avoid situations where this can be hijacked or called separately an SSO `state` whitelist can be provided via the environment variable `SSO_WHITELIST`. This is a comma separated list. For example... +``` +SSO_WHITELIST=https://your.domain +``` +``` +SSO_WHITELIST=https://your.domain,https://your.other.domain +``` - +When set, any requests to log in with a different `state` will be denied.