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

connector: add uaa connector #542

Merged
merged 1 commit into from
Aug 11, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 36 additions & 1 deletion Documentation/connectors-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
158 changes: 158 additions & 0 deletions connector/connector_uaa.go
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

For some reason this is always the empty string. Any idea why this is?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's just the way the test data for UAA is created. When I test against a live server, name is populated as expected. You can hit the /uaa/userinfo API directly to see if name is set or not but I don't think it is based on the UAA docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm wrong. Below is the response from /uaa/userinfo for marissa:

{  
  "user_id":"d9b8dfd8-c420-4243-8b86-d957ef25f97c",
  "user_name":"marissa",
  "given_name":"Marissa",
  "family_name":"Bloggs",
  "email":"marissa@test.org",
  "phone_number":null,
  "name":"Marissa Bloggs"
}

I'll see what is up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can verify that the Name is being parsed properly and given to oidc.Identity. Still digging.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the example-app UI is suggesting that connector/connector_uaa.go#L118 is "" but it's not. When we create the oidc.Identity, the name is properly set to Marissa Bloggs. It seems that when the example-application creates the claims object which gets rendered, it isn't finding this. I'll keep digging but the UAA Connector itself is working properly at the line you referenced.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One thing to note is that the example-app isn't printing out the details of the oidc.Identity created by the connector but is instead printing out the details of the OAuth2 token provided by the provider.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I asked the team working on UAA and they said name is not a standard field: https://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#rfc.section.4.1

Copy link
Contributor

Choose a reason for hiding this comment

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

It's an openid connect thing https://openid.net/specs/openid-connect-basic-1_0.html#StandardClaims

Not a blocker, just wondering.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right but that is for the /uaa/userinfo response and the name is set there. The oidc.Identity object that we craft as a result of calling the UserInfo endpoint does have Name set properly. If you debug the line you pointed to, you'll see this is the case.

What I'm saying is that the JWT token you're inspecting in the example-app is an OAuth2 token and name is not a standard field for that response.

Copy link
Contributor

Choose a reason for hiding this comment

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

This was fixed by #537

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
}
47 changes: 47 additions & 0 deletions connector/connector_uaa_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 1 addition & 0 deletions server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down