Commit bef9f3c2 authored by Eric Chiang's avatar Eric Chiang Committed by GitHub

Merge pull request #542 from whitlockjc/uaa-connector

connector: add uaa connector
parents ac518139 27b80cbc
...@@ -165,7 +165,7 @@ In addition to `id` and `type`, the `ldap` connector takes the following additio ...@@ -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` * emailAttribute: a `string`. Required. Attribute to map to Email. Default: `mail`
* searchBeforeAuth: a `boolean`. Perform search for entryDN to be used for bind. * 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. * 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` * searchScope: a `string`. Scope of the search. `base|one|sub`. Default: `one`
* searchBindDN: a `string`. DN to bind as for search operations. * searchBindDN: a `string`. DN to bind as for search operations.
* searchBindPw: a `string`. Password for bind 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 ...@@ -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 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.
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
}
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)
}
}
...@@ -131,6 +131,7 @@ var connectorDisplayNameMap = map[string]string{ ...@@ -131,6 +131,7 @@ var connectorDisplayNameMap = map[string]string{
"local": "Email", "local": "Email",
"github": "GitHub", "github": "GitHub",
"bitbucket": "Bitbucket", "bitbucket": "Bitbucket",
"uaa": "CloudFoundry User Account and Authentication (UAA)",
} }
type Template interface { type Template interface {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment