Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the “external” connector (for Apache2 mod_auth etc.) #1100

Merged
merged 1 commit into from
Oct 26, 2017

Conversation

stapelberg
Copy link
Contributor

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!

Copy link
Contributor

@srenatus srenatus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite interesting. I'm just a curious bystander (not a dex maintainer), but IMHO this looks good. I wonder a little if it would sense to make the header name configurable?

@ericchiang
Copy link
Contributor

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
Copy link
Contributor Author

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.

Copy link
Contributor

@ericchiang ericchiang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did an initial code review. Biggest concerns are:

  • Lack of a strategy for refresh tokens.
  • Some examples so we can test this.

I'd like to see the refresh token concern addressed before we move forward.

server/server.go Outdated
@@ -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
// "external" connector.
handleFunc("/callback/{connector}", s.handleConnectorCallback)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be the ID of the connector not the type, all connectors can be configured multiple times. I can configure two LDAP connectors with IDs activedirectory and freeipa.

It also needs enforce that handleConnectorCallback only allows a session using whatever value {connector} is. If it's /callback/okta that the user actually logging in through okta.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be the ID of the connector not the type, all connectors can be configured multiple times. I can configure two LDAP connectors with IDs activedirectory and freeipa.

Yes. The /auth/{connector} route assigns connID := mux.Vars(r)["connector"], which is why I chose /callback/{connector} to signify the connector ID. Is that incorrect? Should “connector” be changed to “id”, here and above?

It also needs enforce that handleConnectorCallback only allows a session using whatever value {connector} is. If it's /callback/okta that the user actually logging in through okta.

Done.

}

// Refresh updates the identity during a refresh token request.
func (m *Callback) Refresh(ctx context.Context, s connector.Scopes, identity connector.Identity) (connector.Identity, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this can't be implemented, drop the method. We did this in the OpenID Connect connector because refreshes are hard, but regret it. Better to just not support refresh tokens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if remoteUser == "" {
return connector.Identity{}, fmt.Errorf("required HTTP header X-Remote-User is not set")
}
return connector.Identity{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Groups as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can I derive any groups from X-Remote-User header, which is just an email address?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kubernetes allows additional headers that represent groups. https://kubernetes.io/docs/admin/authentication/#authenticating-proxy

Fine with not addressing this now, but it limits how useful this will be to other services.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks for the pointer. Added a TODO to the code, so that someone can actually verify it works with Kubernetes when they implement it :).

if err != nil {
return "", fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err)
}
u.Path = u.Path + "/external"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, this should probably be passed to the config. Feel free to leave a TODO if it's not obvious.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. On second thought, though: shouldn’t this be derived from the connector ID? I couldn’t see whether connectors have access to the ID they’re configured as.

// Callback is a connector which returns an identity with the HTTP header
// X-Remote-User as verified email.
type Callback struct {
Logger logrus.FieldLogger
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: callback and logger should be lowercase so they're unexported. We only export things we explicitly have a reason to share.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// 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 {
PathSuffix string `json:"pathSuffix"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, this should be passed by the server since it know the URL this connector will operate at. It doesn't need to be configurable by the user.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe let connectors know what their ID is?

func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor Author

@stapelberg stapelberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! Please take another look.

// Callback is a connector which returns an identity with the HTTP header
// X-Remote-User as verified email.
type Callback struct {
Logger logrus.FieldLogger
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if err != nil {
return "", fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err)
}
u.Path = u.Path + "/external"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. On second thought, though: shouldn’t this be derived from the connector ID? I couldn’t see whether connectors have access to the ID they’re configured as.

if remoteUser == "" {
return connector.Identity{}, fmt.Errorf("required HTTP header X-Remote-User is not set")
}
return connector.Identity{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can I derive any groups from X-Remote-User header, which is just an email address?

}

// Refresh updates the identity during a refresh token request.
func (m *Callback) Refresh(ctx context.Context, s connector.Scopes, identity connector.Identity) (connector.Identity, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

server/server.go Outdated
@@ -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
// "external" connector.
handleFunc("/callback/{connector}", s.handleConnectorCallback)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be the ID of the connector not the type, all connectors can be configured multiple times. I can configure two LDAP connectors with IDs activedirectory and freeipa.

Yes. The /auth/{connector} route assigns connID := mux.Vars(r)["connector"], which is why I chose /callback/{connector} to signify the connector ID. Is that incorrect? Should “connector” be changed to “id”, here and above?

It also needs enforce that handleConnectorCallback only allows a session using whatever value {connector} is. If it's /callback/okta that the user actually logging in through okta.

Done.

Copy link
Contributor Author

@stapelberg stapelberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again. Please take another look.

// 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 {
PathSuffix string `json:"pathSuffix"`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if remoteUser == "" {
return connector.Identity{}, fmt.Errorf("required HTTP header X-Remote-User is not set")
}
return connector.Identity{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks for the pointer. Added a TODO to the code, so that someone can actually verify it works with Kubernetes when they implement it :).

@ericchiang
Copy link
Contributor

Code looks fine. Few final requests:

@stapelberg
Copy link
Contributor Author

"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
Copy link
Contributor

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
Copy link
Contributor Author

Got it. I’ll update the pull requests in a few hours.

@stapelberg
Copy link
Contributor Author

All done. Please take another look.

Copy link
Contributor

@ericchiang ericchiang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, though I'm going to follow up with some doc improvements if that's okay.

@ericchiang ericchiang merged commit 751c565 into dexidp:master Oct 26, 2017
# Requires Apache 2.4.10+
RequestHeader set X-Remote-User expr=%{REMOTE_USER}@debian.org

ProxyPass "http://localhost:5556/dex/callback/myBasicAuth"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One concern I just realized. The user can still hit the http://localhost:5556/dex/callback URL to login against the authproxy connector. Is this protected from the user setting X-Remote-User?

This seems dangerous. I'd like to add the following recommendation:

If this connect is used, it's recommended to use this as a sole connector.

I can't really imagine a safe way to allow multiple connectors while this is set...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Let me send a PR which strips X-Remote-User for all dex URLs aside from the correct one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 awesome, thanks

@stapelberg stapelberg deleted the external branch October 26, 2017 17:01
stapelberg added a commit to stapelberg/dex that referenced this pull request Oct 26, 2017
mmrath pushed a commit to mmrath/dex that referenced this pull request Sep 2, 2019
Implement the “external” connector (for Apache2 mod_auth etc.)
mmrath pushed a commit to mmrath/dex that referenced this pull request Sep 2, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants