Repository: coreos/dex
Pull request #1100 (2017-10-21 14:59:51)

Implement the “external” connector (for Apache2 mod_auth etc.)
In Debian, we’re currently investigating whether we can use dex as part of https://sso.debian.org/ (see also https://wiki.debian.org/DebianSingleSignOn).

In the current setup, users authenticate against Apache2, and the web application behind sso.debian.org issues certificates based on the `REMOTE_USER` request environment variable.

Hence, to keep (especially user-visible) changes at a minimum, we’d like to keep the setup the same regarding authentication, just replace the certificates part with dex as an OIDC provider (for GitLab and others).

The “external” connector in this pull request accomplishes that; see also the included documentation.

Please consider merging this pull request. Thanks!

ericchiang: Yeah does look interesting.

Just trying to understand the use cases. You run a proxy, then if the user ever tries to hit `(dex URL)/callback/external` you log the user in, setting the `X-Remote-User` header on the way back?

What's the UX look like for that? How would another provider other than Apache2 mod_auth use this? Do you have examples?
stapelberg: > Just trying to understand the use cases. You run a proxy, then if the user ever tries to hit (dex URL)/callback/external you log the user in, setting the X-Remote-User header on the way back?

This is correct.

> What's the UX look like for that?

This depends on the specific authentication method you have configured in Apache2. Common cases include a modal password prompt (e.g. https://user-images.githubusercontent.com/59393/26989841-371d98c2-4d1a-11e7-8182-6901d76081b5.png), a certificate prompt, some sort of smart-card authentication, etc.

> How would another provider other than Apache2 mod_auth use this? Do you have examples?

Have a look at https://httpd.apache.org/docs/current/howto/auth.html for an overview of how to configure authentication in apache.

Also note that this connector is not specific to apache, it’s just that we’re using apache in Debian. I could also imagine using this connector to integrate with proprietary web servers / appliances in company intranets.
ericchiang: Code looks fine. Few final requests:

* "external" doesn't feel like the correct name. It seems like it should be "authenticatingproxy" or something like that. (open to shorter names)
* A working example in tree that we can use to test this. See our LDAP setup for a good example https://github.com/coreos/dex/blob/master/Documentation/ldap-connector.md#getting-started
* Docs should call out limitations. This mode doesn't support refresh tokens or groups.
stapelberg: > "external" doesn't feel like the correct name. It seems like it should be "authenticatingproxy" or something like that. (open to shorter names)

How about “authproxy” as a shorter version?

> A working example in tree that we can use to test this. See our LDAP setup for a good example https://github.com/coreos/dex/blob/master/Documentation/ldap-connector.md#getting-started

The docs already contain an example Apache config. Are you asking me to add the required config files to actually start a full Apache server, or are you asking me to add a unit test?

> Docs should call out limitations. This mode doesn't support refresh tokens or groups.

Will do.
ericchiang: > How about “authproxy” as a shorter version?

Works for me

> The docs already contain an example Apache config. Are you asking me to add the required config files to actually start a full Apache server, or are you asking me to add a unit test?

A more complete example would be preferable. I've never run an Apache server, for instance. How would I test this? Even links to a basic "getting started" guide would be helpful for future dex maintainers.
stapelberg: Got it. I’ll update the pull requests in a few hours.

Patch:
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+:
+
+```
+<Location /dex/callback/myBasicAuth>
+    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"
+</Location>
+```
+
+## 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`:
+
+```
+<VirtualHost sso.example.net>
+    ServerName sso.example.net
+
+    ServerAdmin webmaster@localhost
+    DocumentRoot /var/www/html
+
+    ErrorLog ${APACHE_LOG_DIR}/error.log
+    CustomLog ${APACHE_LOG_DIR}/access.log combined
+
+    <Location /dex/>
+        ProxyPass "http://localhost:5556/dex/"
+        ProxyPassReverse "http://localhost:5556/dex/"
+    </Location>
+
+    <Location /dex/callback/myBasicAuth>
+        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"
+    </Location>
+</VirtualHost>
+```
+
+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/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/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)
 	}


Test patch:
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_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