outposts: set cookies for a domain to authenticate an entire domain (#971)
* outposts: initial cookie domain implementation Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: add cookie domain setting Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * providers/proxy: replace forward_auth_mode with general mode Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: rebuild proxy provider form Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * providers/proxy: re-add forward_auth_mode for backwards compat Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: fix data.mode not being set Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * root: always set log level to debug when testing Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * providers/proxy: use new mode attribute Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * providers/proxy: only ingress /akprox on forward_domain Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * providers/proxy: fix lint error Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: fix error on ProxyProviderForm when not using proxy mode Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: fix default for outpost form's type missing Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: add additional desc for proxy modes Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts: fix service account permissions not always being updated Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outpost/proxy: fix redirecting to incorrect host for domain mode Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: improve error handling for network errors Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outpost: fix image naming not matching main imaeg Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts/proxy: fix redirects for domain mode and traefik Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: fix colour for paragraphs Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/flows: fix consent stage not showing permissions correctly Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * website/docs: add domain-level docs Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * website/docs: fix broken links Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts/proxy: remove dead code Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/flows: fix missing id for #header-text Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
@ -64,7 +64,7 @@ func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options.
|
||||
providerOpts.SkipAuthRegex = skipRegexes
|
||||
}
|
||||
|
||||
if *provider.ForwardAuthMode {
|
||||
if *provider.Mode == api.PROXYMODE_FORWARD_SINGLE || *provider.Mode == api.PROXYMODE_FORWARD_DOMAIN {
|
||||
providerOpts.UpstreamServers = []options.Upstream{
|
||||
{
|
||||
ID: "static",
|
||||
@ -111,6 +111,10 @@ func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options.
|
||||
func (pb *providerBundle) Build(provider api.ProxyOutpostConfig) {
|
||||
opts := pb.prepareOpts(provider)
|
||||
|
||||
if *provider.Mode == api.PROXYMODE_FORWARD_DOMAIN {
|
||||
opts.Cookie.Domains = []string{*provider.CookieDomain}
|
||||
}
|
||||
|
||||
chain := alice.New()
|
||||
|
||||
if opts.ForceHTTPS {
|
||||
@ -123,10 +127,6 @@ func (pb *providerBundle) Build(provider api.ProxyOutpostConfig) {
|
||||
|
||||
healthCheckPaths := []string{opts.PingPath}
|
||||
healthCheckUserAgents := []string{opts.PingUserAgent}
|
||||
if opts.GCPHealthChecks {
|
||||
healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
|
||||
healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
|
||||
}
|
||||
|
||||
// To silence logging of health checks, register the health check handler before
|
||||
// the logging handler
|
||||
@ -153,6 +153,8 @@ func (pb *providerBundle) Build(provider api.ProxyOutpostConfig) {
|
||||
oauthproxy.BasicAuthPasswordAttribute = *provider.BasicAuthPasswordAttribute
|
||||
}
|
||||
|
||||
oauthproxy.ExternalHost = pb.Host
|
||||
|
||||
pb.proxy = oauthproxy
|
||||
pb.Handler = chain.Then(oauthproxy)
|
||||
}
|
||||
|
@ -106,35 +106,22 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
|
||||
case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
|
||||
redirectURL, err := url.Parse(redirect)
|
||||
if err != nil {
|
||||
p.logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
|
||||
p.logger.WithField("redirect", redirect).Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
|
||||
return false
|
||||
}
|
||||
redirectHostname := redirectURL.Hostname()
|
||||
|
||||
for _, domain := range p.whitelistDomains {
|
||||
domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
|
||||
if domainHostname == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
|
||||
// the domain names match, now validate the ports
|
||||
// if the whitelisted domain's port is '*', allow all ports
|
||||
// if the whitelisted domain contains a specific port, only allow that port
|
||||
// if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
|
||||
redirectPort := redirectURL.Port()
|
||||
if (domainPort == "*") ||
|
||||
(domainPort == redirectPort) ||
|
||||
(domainPort == "" && redirectPort == "") {
|
||||
return true
|
||||
}
|
||||
for _, domain := range p.CookieDomains {
|
||||
if strings.HasSuffix(redirectHostname, domain) {
|
||||
p.logger.WithField("redirect", redirect).WithField("domain", domain).Debug("allowing redirect")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
p.logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
|
||||
p.logger.WithField("redirect", redirect).Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
|
||||
return false
|
||||
default:
|
||||
p.logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
|
||||
p.logger.WithField("redirect", redirect).Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ type OAuthProxy struct {
|
||||
AuthOnlyPath string
|
||||
UserInfoPath string
|
||||
|
||||
forwardAuthMode bool
|
||||
mode api.ProxyMode
|
||||
redirectURL *url.URL // the url to receive requests at
|
||||
whitelistDomains []string
|
||||
provider providers.Provider
|
||||
@ -77,6 +77,7 @@ type OAuthProxy struct {
|
||||
PassUserHeaders bool
|
||||
BasicAuthUserAttribute string
|
||||
BasicAuthPasswordAttribute string
|
||||
ExternalHost string
|
||||
PassAccessToken bool
|
||||
SetAuthorization bool
|
||||
PassAuthorization bool
|
||||
@ -136,7 +137,7 @@ func NewOAuthProxy(opts *options.Options, provider api.ProxyOutpostConfig, c *ht
|
||||
CookieRefresh: opts.Cookie.Refresh,
|
||||
CookieSameSite: opts.Cookie.SameSite,
|
||||
|
||||
forwardAuthMode: *provider.ForwardAuthMode,
|
||||
mode: *provider.Mode,
|
||||
RobotsPath: "/robots.txt",
|
||||
SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
|
||||
SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
|
||||
@ -216,43 +217,6 @@ func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, m
|
||||
}
|
||||
}
|
||||
|
||||
// splitHostPort separates host and port. If the port is not valid, it returns
|
||||
// the entire input as host, and it doesn't check the validity of the host.
|
||||
// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
|
||||
// *** taken from net/url, modified validOptionalPort() to accept ":*"
|
||||
func splitHostPort(hostport string) (host, port string) {
|
||||
host = hostport
|
||||
|
||||
colon := strings.LastIndexByte(host, ':')
|
||||
if colon != -1 && validOptionalPort(host[colon:]) {
|
||||
host, port = host[:colon], host[colon+1:]
|
||||
}
|
||||
|
||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
||||
host = host[1 : len(host)-1]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// validOptionalPort reports whether port is either an empty string
|
||||
// or matches /^:\d*$/
|
||||
// *** taken from net/url, modified to accept ":*"
|
||||
func validOptionalPort(port string) bool {
|
||||
if port == "" || port == ":*" {
|
||||
return true
|
||||
}
|
||||
if port[0] != ':' {
|
||||
return false
|
||||
}
|
||||
for _, b := range port[1:] {
|
||||
if b < '0' || b > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
|
||||
var noCacheHeaders = map[string]string{
|
||||
"Expires": time.Unix(0, 0).Format(time.RFC1123),
|
||||
@ -340,18 +304,41 @@ func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
|
||||
func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
|
||||
session, err := p.getAuthenticatedSession(rw, req)
|
||||
if err != nil {
|
||||
if p.forwardAuthMode {
|
||||
if p.mode == api.PROXYMODE_FORWARD_SINGLE || p.mode == api.PROXYMODE_FORWARD_DOMAIN {
|
||||
if _, ok := req.URL.Query()["nginx"]; ok {
|
||||
rw.WriteHeader(401)
|
||||
return
|
||||
}
|
||||
if _, ok := req.URL.Query()["traefik"]; ok {
|
||||
host := getHost(req)
|
||||
host := ""
|
||||
// Optional suffix, which is appended to the URL
|
||||
suffix := ""
|
||||
if p.mode == api.PROXYMODE_FORWARD_SINGLE {
|
||||
host = getHost(req)
|
||||
} else if p.mode == api.PROXYMODE_FORWARD_DOMAIN {
|
||||
host = p.ExternalHost
|
||||
// set the ?rd flag to the current URL we have, since we redirect
|
||||
// to a (possibly) different domain, but we want to be redirected back
|
||||
// to the application
|
||||
v := url.Values{
|
||||
// see https://doc.traefik.io/traefik/middlewares/forwardauth/
|
||||
// X-Forwarded-Uri is only the path, so we need to build the entire URL
|
||||
"rd": []string{fmt.Sprintf(
|
||||
"%s://%s%s",
|
||||
req.Header.Get("X-Forwarded-Proto"),
|
||||
req.Header.Get("X-Forwarded-Host"),
|
||||
req.Header.Get("X-Forwarded-Uri"),
|
||||
)},
|
||||
}
|
||||
suffix = fmt.Sprintf("?%s", v.Encode())
|
||||
}
|
||||
proto := req.Header.Get("X-Forwarded-Proto")
|
||||
if proto != "" {
|
||||
proto = proto + ":"
|
||||
}
|
||||
http.Redirect(rw, req, fmt.Sprintf("%s//%s%s", proto, host, p.OAuthStartPath), http.StatusTemporaryRedirect)
|
||||
rdFinal := fmt.Sprintf("%s//%s%s%s", proto, host, p.OAuthStartPath, suffix)
|
||||
p.logger.WithField("url", rdFinal).Debug("Redirecting to login")
|
||||
http.Redirect(rw, req, rdFinal, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -360,7 +347,7 @@ func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
// we are authenticated
|
||||
p.addHeadersForProxying(rw, req, session)
|
||||
if p.forwardAuthMode {
|
||||
if p.mode == api.PROXYMODE_FORWARD_SINGLE || p.mode == api.PROXYMODE_FORWARD_DOMAIN {
|
||||
for headerKey, headers := range req.Header {
|
||||
for _, value := range headers {
|
||||
rw.Header().Set(headerKey, value)
|
||||
|
Reference in New Issue
Block a user