diff --git a/Documentation/connectors-configuration.md b/Documentation/connectors-configuration.md index f00b47b1f5..0970f30860 100644 --- a/Documentation/connectors-configuration.md +++ b/Documentation/connectors-configuration.md @@ -165,7 +165,7 @@ In addition to `id` and `type`, the `ldap` connector takes the following additio * emailAttribute: a `string`. Required. Attribute to map to Email. Default: `mail` * searchBeforeAuth: a `boolean`. Perform search for entryDN to be used for bind. * searchFilter: a `string`. Filter to apply to search. Variable substititions: `%u` User supplied username/e-mail address. `%b` BaseDN. Searches that return multiple entries are considered ambiguous and will return an error. -* searchGroupFilter: a `string`. A filter which should return group entry for a given user. The string is formatted the same as `searchFilter`, execpt `%u` is replaced by the fully qualified user entry. Groups are only searched if the client request the "groups" scope. +* searchGroupFilter: a `string`. A filter which should return group entry for a given user. The string is formatted the same as `searchFilter`, execpt `%u` is replaced by the fully qualified user entry. Groups are only searched if the client request the "groups" scope. * searchScope: a `string`. Scope of the search. `base|one|sub`. Default: `one` * searchBindDN: a `string`. DN to bind as for search operations. * searchBindPw: a `string`. Password for bind for search operations. @@ -237,3 +237,38 @@ To set a connectors configuration in dex, put it in some temporary file, then us ``` dexctl --db-url=$DEX_DB_URL set-connector-configs /tmp/dex_connectors.json ``` + +### `uaa` connector + +This connector config lets users authenticate through the +[CloudFoundry User Account and Authentication (UAA) Server](https://github.com/cloudfoundry/uaa). In addition to `id` +and `type`, the `uaa` connector takes the following additional fields: + +* clientID: a `string`. The UAA OAuth application client ID. +* clientSecret: a `string`. The UAA OAuth application client secret. +* serverURL: a `string`. The full URL to the UAA service. + +To begin, register an OAuth application with UAA. To register dex as a client of your UAA application, there are two +things your OAuth application needs to have configured properly: + +* Make sure dex's redirect URL _(`ISSUER_URL/auth/$CONNECTOR_ID/callback`)_ is in the application's `redirect_uri` list +* Make sure the `openid` scope is in the application's `scope` list + +Regarding the `redirect_uri` list, as an example if you were running dex at `https://auth.example.com/bar`, the UAA +OAuth application's `redirect_uri` list would need to contain `https://auth.example.com/bar/auth/uaa/callback`. + +Here's an example of a `uaa` connector _(The `clientID` and `clientSecret` should be replaced by values provided to UAA +and the `serverURL` should be the fully-qualified URL to your UAA server)_: + +``` + { + "type": "uaa", + "id": "example-uaa", + "clientID": "$UAA_OAUTH_APPLICATION_CLIENT_ID", + "clientSecret": "$UAA_OAUTH_APPLICATION_CLIENT_SECRET", + "serverURL": "$UAA_SERVER_URL" + } +``` + +The `uaa` connector requests only the `openid` scope which allows dex the ability to query the user's identity +information. diff --git a/connector/connector_uaa.go b/connector/connector_uaa.go new file mode 100644 index 0000000000..7d7bf6de0b --- /dev/null +++ b/connector/connector_uaa.go @@ -0,0 +1,158 @@ +package connector + +import ( + "encoding/json" + "fmt" + "html/template" + "net/http" + "net/url" + "path" + + chttp "github.com/coreos/go-oidc/http" + "github.com/coreos/go-oidc/oauth2" + "github.com/coreos/go-oidc/oidc" +) + +const ( + UAAConnectorType = "uaa" +) + +type UAAConnectorConfig struct { + ID string `json:"id"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + ServerURL string `json:"serverURL"` +} + +// standard error form returned by UAA +type uaaError struct { + ErrorDescription string `json:"error_description"` + ErrorType string `json:"error"` +} + +type uaaOAuth2Connector struct { + clientID string + clientSecret string + client *oauth2.Client + uaaBaseURL *url.URL +} + +func init() { + RegisterConnectorConfigType(UAAConnectorType, func() ConnectorConfig { return &UAAConnectorConfig{} }) +} + +func (cfg *UAAConnectorConfig) ConnectorID() string { + return cfg.ID +} + +func (cfg *UAAConnectorConfig) ConnectorType() string { + return UAAConnectorType +} + +func (cfg *UAAConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *template.Template) (Connector, error) { + uaaBaseURL, err := url.ParseRequestURI(cfg.ServerURL) + if err != nil { + return nil, fmt.Errorf("Invalid configuration. UAA URL is invalid: %v", err) + } + if !uaaBaseURL.IsAbs() { + return nil, fmt.Errorf("Invalid configuration. UAA URL must be absolute") + } + ns.Path = path.Join(ns.Path, httpPathCallback) + oauth2Conn, err := newUAAConnector(cfg, uaaBaseURL, ns.String()) + if err != nil { + return nil, err + } + return &OAuth2Connector{ + id: cfg.ID, + loginFunc: lf, + cbURL: ns, + conn: oauth2Conn, + }, nil +} + +func (err uaaError) Error() string { + return fmt.Sprintf("uaa (%s): %s", err.ErrorType, err.ErrorDescription) +} + +func (c *uaaOAuth2Connector) Client() *oauth2.Client { + return c.client +} + +func (c *uaaOAuth2Connector) Healthy() error { + return nil +} + +func (c *uaaOAuth2Connector) Identity(cli chttp.Client) (oidc.Identity, error) { + uaaUserInfoURL := *c.uaaBaseURL + uaaUserInfoURL.Path = path.Join(uaaUserInfoURL.Path, "/userinfo") + req, err := http.NewRequest("GET", uaaUserInfoURL.String(), nil) + if err != nil { + return oidc.Identity{}, err + } + resp, err := cli.Do(req) + if err != nil { + return oidc.Identity{}, fmt.Errorf("get: %v", err) + } + defer resp.Body.Close() + switch { + case resp.StatusCode >= 400 && resp.StatusCode < 600: + // attempt to decode error from UAA + var authErr uaaError + if err := json.NewDecoder(resp.Body).Decode(&authErr); err != nil { + return oidc.Identity{}, oauth2.NewError(oauth2.ErrorAccessDenied) + } + return oidc.Identity{}, authErr + case resp.StatusCode == http.StatusOK: + default: + return oidc.Identity{}, fmt.Errorf("unexpected status from providor %s", resp.Status) + } + var user struct { + UserID string `json:"user_id"` + Email string `json:"email"` + Name string `json:"name"` + UserName string `json:"user_name"` + } + if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { + return oidc.Identity{}, fmt.Errorf("getting user info: %v", err) + } + name := user.Name + if name == "" { + name = user.UserName + } + return oidc.Identity{ + ID: user.UserID, + Name: name, + Email: user.Email, + }, nil +} + +func (c *uaaOAuth2Connector) TrustedEmailProvider() bool { + return false +} + +func newUAAConnector(cfg *UAAConnectorConfig, uaaBaseURL *url.URL, cbURL string) (oauth2Connector, error) { + uaaAuthURL := *uaaBaseURL + uaaTokenURL := *uaaBaseURL + uaaAuthURL.Path = path.Join(uaaAuthURL.Path, "/oauth/authorize") + uaaTokenURL.Path = path.Join(uaaTokenURL.Path, "/oauth/token") + config := oauth2.Config{ + Credentials: oauth2.ClientCredentials{ID: cfg.ClientID, Secret: cfg.ClientSecret}, + AuthURL: uaaAuthURL.String(), + TokenURL: uaaTokenURL.String(), + Scope: []string{"openid"}, + AuthMethod: oauth2.AuthMethodClientSecretPost, + RedirectURL: cbURL, + } + + cli, err := oauth2.NewClient(http.DefaultClient, config) + if err != nil { + return nil, err + } + + return &uaaOAuth2Connector{ + clientID: cfg.ClientID, + clientSecret: cfg.ClientSecret, + client: cli, + uaaBaseURL: uaaBaseURL, + }, nil +} diff --git a/connector/connector_uaa_test.go b/connector/connector_uaa_test.go new file mode 100644 index 0000000000..361b0f2c82 --- /dev/null +++ b/connector/connector_uaa_test.go @@ -0,0 +1,47 @@ +package connector + +import ( + "testing" +) + +func TestUAAConnectorConfigInvalidserverURLNotAValidURL(t *testing.T) { + cc := UAAConnectorConfig{ + ID: "uaa", + ClientID: "test-client", + ClientSecret: "test-client-secret", + ServerURL: "https//login.apigee.com", + } + + _, err := cc.Connector(ns, lf, templates) + if err == nil { + t.Fatal("Expected UAAConnector initialization to fail when UAA URL is an invalid URL") + } +} + +func TestUAAConnectorConfigInvalidserverURLNotAbsolute(t *testing.T) { + cc := UAAConnectorConfig{ + ID: "uaa", + ClientID: "test-client", + ClientSecret: "test-client-secret", + ServerURL: "/uaa", + } + + _, err := cc.Connector(ns, lf, templates) + if err == nil { + t.Fatal("Expected UAAConnector initialization to fail when UAA URL is not an aboslute URL") + } +} + +func TestUAAConnectorConfigValidserverURL(t *testing.T) { + cc := UAAConnectorConfig{ + ID: "uaa", + ClientID: "test-client", + ClientSecret: "test-client-secret", + ServerURL: "https://login.apigee.com", + } + + _, err := cc.Connector(ns, lf, templates) + if err != nil { + t.Fatal(err) + } +} diff --git a/server/http.go b/server/http.go index 4616389a16..b3a7cc866a 100644 --- a/server/http.go +++ b/server/http.go @@ -131,6 +131,7 @@ var connectorDisplayNameMap = map[string]string{ "local": "Email", "github": "GitHub", "bitbucket": "Bitbucket", + "uaa": "CloudFoundry User Account and Authentication (UAA)", } type Template interface {