-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Conversation
There was a problem hiding this 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?
Yeah does look interesting. Just trying to understand the use cases. You run a proxy, then if the user ever tries to hit What's the UX look like for that? How would another provider other than Apache2 mod_auth use this? Do you have examples? |
This is correct.
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.
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. |
There was a problem hiding this 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) |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
connector/external/external.go
Outdated
} | ||
|
||
// 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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
connector/external/external.go
Outdated
if remoteUser == "" { | ||
return connector.Identity{}, fmt.Errorf("required HTTP header X-Remote-User is not set") | ||
} | ||
return connector.Identity{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Groups as well?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :).
connector/external/external.go
Outdated
if err != nil { | ||
return "", fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err) | ||
} | ||
u.Path = u.Path + "/external" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
connector/external/external.go
Outdated
// Callback is a connector which returns an identity with the HTTP header | ||
// X-Remote-User as verified email. | ||
type Callback struct { | ||
Logger logrus.FieldLogger |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
connector/external/external.go
Outdated
// 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"` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this 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.
connector/external/external.go
Outdated
// Callback is a connector which returns an identity with the HTTP header | ||
// X-Remote-User as verified email. | ||
type Callback struct { | ||
Logger logrus.FieldLogger |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
connector/external/external.go
Outdated
if err != nil { | ||
return "", fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err) | ||
} | ||
u.Path = u.Path + "/external" |
There was a problem hiding this comment.
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.
connector/external/external.go
Outdated
if remoteUser == "" { | ||
return connector.Identity{}, fmt.Errorf("required HTTP header X-Remote-User is not set") | ||
} | ||
return connector.Identity{ |
There was a problem hiding this comment.
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?
connector/external/external.go
Outdated
} | ||
|
||
// 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) { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
connector/external/external.go
Outdated
// 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"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
connector/external/external.go
Outdated
if remoteUser == "" { | ||
return connector.Identity{}, fmt.Errorf("required HTTP header X-Remote-User is not set") | ||
} | ||
return connector.Identity{ |
There was a problem hiding this comment.
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 :).
Code looks fine. Few final requests:
|
How about “authproxy” as a shorter version?
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?
Will do. |
Works for me
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. |
Got it. I’ll update the pull requests in a few hours. |
All done. Please take another look. |
There was a problem hiding this 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.
# Requires Apache 2.4.10+ | ||
RequestHeader set X-Remote-User expr=%{REMOTE_USER}@debian.org | ||
|
||
ProxyPass "http://localhost:5556/dex/callback/myBasicAuth" |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @stapelberg
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 awesome, thanks
follow-up for dexidp#1100
Implement the “external” connector (for Apache2 mod_auth etc.)
follow-up for dexidp#1100
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!