Merge pull request #542 from whitlockjc/uaa-connector
connector: add uaa connector
This commit is contained in:
commit
bef9f3c221
4 changed files with 242 additions and 1 deletions
|
@ -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.
|
||||||
|
|
158
connector/connector_uaa.go
Normal file
158
connector/connector_uaa.go
Normal file
|
@ -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
|
||||||
|
}
|
47
connector/connector_uaa_test.go
Normal file
47
connector/connector_uaa_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
Reference in a new issue