From a41d93db4aa763c6c64421770cd2aefcc2ddb753 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sat, 21 Oct 2017 16:54:54 +0200 Subject: [PATCH] =?UTF-8?q?Implement=20the=20=E2=80=9Cauthproxy=E2=80=9D?= =?UTF-8?q?=20connector=20(for=20Apache2=20mod=5Fauth=20etc.)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Documentation/authproxy.md | 87 ++++++++++++++++++++++++++++++++ README.md | 1 + connector/authproxy/authproxy.go | 56 ++++++++++++++++++++ connector/github/github.go | 2 +- connector/gitlab/gitlab.go | 2 +- connector/ldap/ldap.go | 2 +- connector/mock/connectortest.go | 4 +- connector/oidc/oidc.go | 2 +- connector/oidc/oidc_test.go | 7 +-- connector/saml/saml.go | 2 +- server/handlers.go | 6 +++ server/server.go | 9 +++- 12 files changed, 168 insertions(+), 12 deletions(-) create mode 100644 Documentation/authproxy.md create mode 100644 connector/authproxy/authproxy.go diff --git a/Documentation/authproxy.md b/Documentation/authproxy.md new file mode 100644 index 00000000..10ff1980 --- /dev/null +++ b/Documentation/authproxy.md @@ -0,0 +1,87 @@ +# External authentication + +## Overview + +The authproxy connector returns identities based on authentication which your +front-end web server performs. + +The connector does not support refresh tokens or groups at this point. + +## Configuration + +The following is an example config file that can be used by the external +connector to authenticate a user. + +```yaml +connectors: +- type: authproxy + id: myBasicAuth + name: HTTP Basic Auth +``` + +The authproxy connector assumes that you configured your front-end web server +such that it performs authentication for the `/dex/callback/myBasicAuth` +location and provides the result in the X-Remote-User HTTP header. The following +configuration will work for Apache 2.4.10+: + +``` + + AuthType Basic + AuthName "db.debian.org webPassword" + AuthBasicProvider file + AuthUserFile "/etc/apache2/debian-web-pw.htpasswd" + Require valid-user + + # Defense in depth: clear the Authorization header so that + # Debian Web Passwords never even reach dex. + RequestHeader unset Authorization + + # Requires Apache 2.4.10+ + RequestHeader set X-Remote-User expr=%{REMOTE_USER}@debian.org + + ProxyPass "http://localhost:5556/dex/callback/myBasicAuth" + ProxyPassReverse "http://localhost:5556/dex/callback/myBasicAuth" + +``` + +## Full Apache2 setup + +After installing your Linux distribution’s Apache2 package, place the following +virtual host configuration in e.g. `/etc/apache2/sites-available/sso.conf`: + +``` + + ServerName sso.example.net + + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + + ProxyPass "http://localhost:5556/dex/" + ProxyPassReverse "http://localhost:5556/dex/" + + + + AuthType Basic + AuthName "db.debian.org webPassword" + AuthBasicProvider file + AuthUserFile "/etc/apache2/debian-web-pw.htpasswd" + Require valid-user + + # Defense in depth: clear the Authorization header so that + # Debian Web Passwords never even reach dex. + RequestHeader unset Authorization + + # Requires Apache 2.4.10+ + RequestHeader set X-Remote-User expr=%{REMOTE_USER}@debian.org + + ProxyPass "http://localhost:5556/dex/callback/myBasicAuth" + ProxyPassReverse "http://localhost:5556/dex/callback/myBasicAuth" + + +``` + +Then, enable it using `a2ensite sso.conf`, followed by a restart of Apache2. \ No newline at end of file diff --git a/README.md b/README.md index 1474b17a..84b39725 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ More docs for running dex as a Kubernetes authenticator can be found [here](Docu * [GitLab](Documentation/gitlab-connector.md) * [SAML 2.0](Documentation/saml-connector.md) * [OpenID Connect](Documentation/oidc-connector.md) (includes Google, Salesforce, Azure, etc.) + * [authproxy](Documentation/authproxy.md) (Apache2 mod_auth, etc.) * Client libraries * [Go][go-oidc] diff --git a/connector/authproxy/authproxy.go b/connector/authproxy/authproxy.go new file mode 100644 index 00000000..a895789d --- /dev/null +++ b/connector/authproxy/authproxy.go @@ -0,0 +1,56 @@ +// Package authproxy implements a connector which relies on external +// authentication (e.g. mod_auth in Apache2) and returns an identity with the +// HTTP header X-Remote-User as verified email. +package authproxy + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/coreos/dex/connector" + "github.com/sirupsen/logrus" +) + +// Config holds the configuration parameters for a connector which returns an +// identity with the HTTP header X-Remote-User as verified email. +type Config struct{} + +// Open returns an authentication strategy which requires no user interaction. +func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) { + return &callback{logger: logger, pathSuffix: "/" + id}, nil +} + +// Callback is a connector which returns an identity with the HTTP header +// X-Remote-User as verified email. +type callback struct { + logger logrus.FieldLogger + pathSuffix string +} + +// LoginURL returns the URL to redirect the user to login with. +func (m *callback) LoginURL(s connector.Scopes, callbackURL, state string) (string, error) { + u, err := url.Parse(callbackURL) + if err != nil { + return "", fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err) + } + u.Path = u.Path + m.pathSuffix + v := u.Query() + v.Set("state", state) + u.RawQuery = v.Encode() + return u.String(), nil +} + +// HandleCallback parses the request and returns the user's identity +func (m *callback) HandleCallback(s connector.Scopes, r *http.Request) (connector.Identity, error) { + remoteUser := r.Header.Get("X-Remote-User") + if remoteUser == "" { + return connector.Identity{}, fmt.Errorf("required HTTP header X-Remote-User is not set") + } + // TODO: add support for X-Remote-Group, see + // https://kubernetes.io/docs/admin/authentication/#authenticating-proxy + return connector.Identity{ + Email: remoteUser, + EmailVerified: true, + }, nil +} diff --git a/connector/github/github.go b/connector/github/github.go index ae078412..fc578aeb 100644 --- a/connector/github/github.go +++ b/connector/github/github.go @@ -63,7 +63,7 @@ type Org struct { } // Open returns a strategy for logging in through GitHub. -func (c *Config) Open(logger logrus.FieldLogger) (connector.Connector, error) { +func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) { if c.Org != "" { // Return error if both 'org' and 'orgs' fields are used. diff --git a/connector/gitlab/gitlab.go b/connector/gitlab/gitlab.go index a4da0197..f6cbbf09 100644 --- a/connector/gitlab/gitlab.go +++ b/connector/gitlab/gitlab.go @@ -51,7 +51,7 @@ type gitlabGroup struct { } // Open returns a strategy for logging in through GitLab. -func (c *Config) Open(logger logrus.FieldLogger) (connector.Connector, error) { +func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) { if c.BaseURL == "" { c.BaseURL = "https://www.gitlab.com" } diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go index 01125ae7..5d19a51d 100644 --- a/connector/ldap/ldap.go +++ b/connector/ldap/ldap.go @@ -153,7 +153,7 @@ func parseScope(s string) (int, bool) { } // Open returns an authentication strategy using LDAP. -func (c *Config) Open(logger logrus.FieldLogger) (connector.Connector, error) { +func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) { conn, err := c.OpenConnector(logger) if err != nil { return nil, err diff --git a/connector/mock/connectortest.go b/connector/mock/connectortest.go index 7236230a..18abd820 100644 --- a/connector/mock/connectortest.go +++ b/connector/mock/connectortest.go @@ -69,7 +69,7 @@ func (m *Callback) Refresh(ctx context.Context, s connector.Scopes, identity con type CallbackConfig struct{} // Open returns an authentication strategy which requires no user interaction. -func (c *CallbackConfig) Open(logger logrus.FieldLogger) (connector.Connector, error) { +func (c *CallbackConfig) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) { return NewCallbackConnector(logger), nil } @@ -81,7 +81,7 @@ type PasswordConfig struct { } // Open returns an authentication strategy which prompts for a predefined username and password. -func (c *PasswordConfig) Open(logger logrus.FieldLogger) (connector.Connector, error) { +func (c *PasswordConfig) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) { if c.Username == "" { return nil, errors.New("no username supplied") } diff --git a/connector/oidc/oidc.go b/connector/oidc/oidc.go index 9cf7a3be..f0d8daf7 100644 --- a/connector/oidc/oidc.go +++ b/connector/oidc/oidc.go @@ -75,7 +75,7 @@ func registerBrokenAuthHeaderProvider(url string) { // Open returns a connector which can be used to login users through an upstream // OpenID Connect provider. -func (c *Config) Open(logger logrus.FieldLogger) (conn connector.Connector, err error) { +func (c *Config) Open(id string, logger logrus.FieldLogger) (conn connector.Connector, err error) { ctx, cancel := context.WithCancel(context.Background()) provider, err := oidc.NewProvider(ctx, c.Issuer) diff --git a/connector/oidc/oidc_test.go b/connector/oidc/oidc_test.go index 305ab2ce..18838efa 100644 --- a/connector/oidc/oidc_test.go +++ b/connector/oidc/oidc_test.go @@ -1,12 +1,13 @@ package oidc import ( - "github.com/coreos/dex/connector" - "github.com/sirupsen/logrus" "net/url" "os" "reflect" "testing" + + "github.com/coreos/dex/connector" + "github.com/sirupsen/logrus" ) func TestKnownBrokenAuthHeaderProvider(t *testing.T) { @@ -73,7 +74,7 @@ func TestOidcConnector_LoginURL(t *testing.T) { HostedDomains: test.hostedDomains, } - conn, err := config.Open(logger) + conn, err := config.Open("oidc", logger) if err != nil { t.Errorf("failed to open connector: %v", err) continue diff --git a/connector/saml/saml.go b/connector/saml/saml.go index f35f0a81..02b3fad7 100644 --- a/connector/saml/saml.go +++ b/connector/saml/saml.go @@ -125,7 +125,7 @@ func (c certStore) Certificates() (roots []*x509.Certificate, err error) { // Open validates the config and returns a connector. It does not actually // validate connectivity with the provider. -func (c *Config) Open(logger logrus.FieldLogger) (connector.Connector, error) { +func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) { return c.openConnector(logger) } diff --git a/server/handlers.go b/server/handlers.go index a70858a9..345cd496 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -346,6 +346,12 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) return } + if connID := mux.Vars(r)["connector"]; connID != "" && connID != authReq.ConnectorID { + s.logger.Errorf("Connector mismatch: authentication started with id %q, but callback for id %q was triggered", authReq.ConnectorID, connID) + s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") + return + } + conn, err := s.getConnector(authReq.ConnectorID) if err != nil { s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err) diff --git a/server/server.go b/server/server.go index aabe7abe..d1e1ff56 100644 --- a/server/server.go +++ b/server/server.go @@ -19,6 +19,7 @@ import ( "github.com/sirupsen/logrus" "github.com/coreos/dex/connector" + "github.com/coreos/dex/connector/authproxy" "github.com/coreos/dex/connector/github" "github.com/coreos/dex/connector/gitlab" "github.com/coreos/dex/connector/ldap" @@ -240,6 +241,9 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) handleFunc("/auth", s.handleAuthorization) handleFunc("/auth/{connector}", s.handleConnectorLogin) handleFunc("/callback", s.handleConnectorCallback) + // For easier connector-specific web server configuration, e.g. for the + // "authproxy" connector. + handleFunc("/callback/{connector}", s.handleConnectorCallback) handleFunc("/approval", s.handleApproval) handleFunc("/healthz", s.handleHealth) handlePrefix("/static", static) @@ -381,7 +385,7 @@ func (s *Server) startGarbageCollection(ctx context.Context, frequency time.Dura // ConnectorConfig is a configuration that can open a connector. type ConnectorConfig interface { - Open(logrus.FieldLogger) (connector.Connector, error) + Open(id string, logger logrus.FieldLogger) (connector.Connector, error) } // ConnectorsConfig variable provides an easy way to return a config struct @@ -394,6 +398,7 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{ "gitlab": func() ConnectorConfig { return new(gitlab.Config) }, "oidc": func() ConnectorConfig { return new(oidc.Config) }, "saml": func() ConnectorConfig { return new(saml.Config) }, + "authproxy": func() ConnectorConfig { return new(authproxy.Config) }, // Keep around for backwards compatibility. "samlExperimental": func() ConnectorConfig { return new(saml.Config) }, } @@ -415,7 +420,7 @@ func openConnector(logger logrus.FieldLogger, conn storage.Connector) (connector } } - c, err := connConfig.Open(logger) + c, err := connConfig.Open(conn.ID, logger) if err != nil { return c, fmt.Errorf("failed to create connector %s: %v", conn.ID, err) }