diff --git a/Documentation/connectors-configuration.md b/Documentation/connectors-configuration.md index 63842656..92d35a04 100644 --- a/Documentation/connectors-configuration.md +++ b/Documentation/connectors-configuration.md @@ -273,3 +273,49 @@ and the `serverURL` should be the fully-qualified URL to your UAA server)_: The `uaa` connector requests only the `openid` scope which allows dex the ability to query the user's identity information. + +### `facebook` connector + +This connector config lets users authenticate through [Facebook](https://www.facebook.com/). In addition to `id` and `type`, the `facebook` connector takes the following additional fields: + +* clientID: a `string`. The Facebook App ID. +* clientSecret: a `string`. The Facebook App Secret. + +To begin, register an App in facebook and configure it according to following steps. + +* Go to [https://developers.facebook.com/](https://developers.facebook.com/) and log in using your Facebook credentials. +* If you haven't created developer account follow step 2 in [https://developers.facebook.com/docs/apps/register](https://developers.facebook.com/docs/apps/register). +* Click on `My Apps` and then click `Create a New App`. +* Choose the platform you wish to use. Select `Website` if you are testing dex sample app. +* Enter the name of your new app in the window that appears and click `Create App ID`. +* Enter a `Display Name`, `Contact Email` and select an appropriate `category` from the dropdown. Click `Create App ID`. +* Click on `Dashboard` from the left menu to go to the developer Dashboard. You can find the `App ID` and `App Secret` there. Click Show to view the `App Secret`. +* Click `Settings` on the left menu and navigate to the Basic tab. Add the dex worker domain(if dex is running on localhost, you can add `localhost` as the `App Domain`) and click `Add Platform`. +* Select `Website` as the platform for the application and enter the dex URL (if dex is running on localhost, you can add `http://localhost:5556`). Click `Save Changes`. +* On the left panel, click `Add Product` and click Get Started for a `Facebook Login` product. +* You can configure the Client OAuth Settings on the window that appears. `Client OAuth Login` should be set to `Yes`. `Web OAuth Login` should be set to `Yes`. `Valid OAuth redirect URIs` should be set to in following format. + +``` +$ISSUER_URL/auth/$CONNECTOR_ID/callback +``` + +For example runnning a connector with ID `"facebook"` and an issuer URL of `"https://auth.example.com/spaz"` the redirect would be. + +``` +https://auth.example.com/spaz/auth/facebook/callback +``` + +* Scroll down and click the Save Changes button to save the change. + + +Here's an example of a `facebook` connector configuration; the clientID and clientSecret should be replaced by App ID and App Secret values provided by Facebook. + +``` + { + "type": "facebook", + "id": "facebook", + "clientID": "$DEX_FACEBOOK_CLIENT_ID", + "clientSecret": "$DEX_FACEBOOK_CLIENT_SECRET" + } +``` + diff --git a/connector/connector_facebook.go b/connector/connector_facebook.go new file mode 100644 index 00000000..5b858e2c --- /dev/null +++ b/connector/connector_facebook.go @@ -0,0 +1,148 @@ +package connector + +import ( + "encoding/json" + "fmt" + chttp "github.com/coreos/go-oidc/http" + "github.com/coreos/go-oidc/oauth2" + "github.com/coreos/go-oidc/oidc" + "html/template" + "net/http" + "net/url" + "path" +) + +const ( + FacebookConnectorType = "facebook" + facebookConnectorAuthURL = "https://www.facebook.com/dialog/oauth" + facebookTokenURL = "https://graph.facebook.com/v2.3/oauth/access_token" + facebookGraphAPIURL = "https://graph.facebook.com/me?fields=id,name,email" +) + +type FacebookConnectorConfig struct { + ID string `json:"id"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` +} + +func init() { + RegisterConnectorConfigType(FacebookConnectorType, func() ConnectorConfig { return &FacebookConnectorConfig{} }) +} + +func (cfg *FacebookConnectorConfig) ConnectorID() string { + return cfg.ID +} + +func (cfg *FacebookConnectorConfig) ConnectorType() string { + return FacebookConnectorType +} + +func (cfg *FacebookConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *template.Template) (Connector, error) { + ns.Path = path.Join(ns.Path, httpPathCallback) + oauth2Conn, err := newFacebookConnector(cfg.ClientID, cfg.ClientSecret, ns.String()) + if err != nil { + return nil, err + } + return &OAuth2Connector{ + id: cfg.ID, + loginFunc: lf, + cbURL: ns, + conn: oauth2Conn, + }, nil +} + +type facebookOAuth2Connector struct { + clientID string + clientSecret string + client *oauth2.Client +} + +func newFacebookConnector(clientID, clientSecret, cbURL string) (oauth2Connector, error) { + config := oauth2.Config{ + Credentials: oauth2.ClientCredentials{ID: clientID, Secret: clientSecret}, + AuthURL: facebookConnectorAuthURL, + TokenURL: facebookTokenURL, + AuthMethod: oauth2.AuthMethodClientSecretPost, + RedirectURL: cbURL, + Scope: []string{"email"}, + } + + cli, err := oauth2.NewClient(http.DefaultClient, config) + if err != nil { + return nil, err + } + + return &facebookOAuth2Connector{ + clientID: clientID, + clientSecret: clientSecret, + client: cli, + }, nil +} +func (c *facebookOAuth2Connector) Client() *oauth2.Client { + return c.client +} + +func (c *facebookOAuth2Connector) Healthy() error { + return nil +} + +func (c *facebookOAuth2Connector) TrustedEmailProvider() bool { + return false +} + +type ErrorMessage struct { + Message string `json:"message"` + Type string `json:"type"` + Code int `json:"code"` + ErrorSubCode int `json:"error_subcode"` + ErrorUserTitle string `json:"error_user_title"` + ErrorUserMsg string `json:"error_user_msg"` + FbTraceId string `json:"fbtrace_id"` +} + +type facebookErr struct { + ErrorMessage ErrorMessage `json:"error"` +} + +func (err facebookErr) Error() string { + return fmt.Sprintf("facebook: %s", err.ErrorMessage.Message) +} + +func (c *facebookOAuth2Connector) Identity(cli chttp.Client) (oidc.Identity, error) { + var user struct { + ID string `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + } + + req, err := http.NewRequest("GET", facebookGraphAPIURL, 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: + var authErr facebookErr + 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) + } + if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { + return oidc.Identity{}, fmt.Errorf("decode body: %v", err) + } + + return oidc.Identity{ + ID: user.ID, + Name: user.Name, + Email: user.Email, + }, nil +} diff --git a/connector/connector_facebook_test.go b/connector/connector_facebook_test.go new file mode 100644 index 00000000..70549b00 --- /dev/null +++ b/connector/connector_facebook_test.go @@ -0,0 +1,71 @@ +package connector + +import ( + "github.com/coreos/go-oidc/oidc" + "net/http" + "testing" +) + +var facebookUser1 = `{ + "id":"testUser1", + "name":"testUser1Fname testUser1Lname", + "email": "testUser1@facebook.com" + }` + +var facebookUser2 = `{ + "id":"testUser2", + "name":"testUser2Fname testUser2Lname", + "email": "testUser2@facebook.com" + }` + +var facebookExampleError = `{ + "error": { + "message": "Invalid OAuth access token signature.", + "type": "OAuthException", + "code": 190, + "fbtrace_id": "Ee/6W0EfrWP" + } +}` + +func TestFacebookIdentity(t *testing.T) { + tests := []oauth2IdentityTest{ + { + urlResps: map[string]response{ + facebookGraphAPIURL: {http.StatusOK, facebookUser1}, + }, + want: oidc.Identity{ + Name: "testUser1Fname testUser1Lname", + ID: "testUser1", + Email: "testUser1@facebook.com", + }, + }, + { + urlResps: map[string]response{ + facebookGraphAPIURL: {http.StatusOK, facebookUser2}, + }, + want: oidc.Identity{ + Name: "testUser2Fname testUser2Lname", + ID: "testUser2", + Email: "testUser2@facebook.com", + }, + }, + { + urlResps: map[string]response{ + facebookGraphAPIURL: {http.StatusUnauthorized, facebookExampleError}, + }, + wantErr: facebookErr{ + ErrorMessage: ErrorMessage{ + Code: 190, + Type: "OAuthException", + Message: "Invalid OAuth access token signature.", + FbTraceId: "Ee/6W0EfrWP", + }, + }, + }, + } + conn, err := newFacebookConnector("fakeFacebookAppID", "fakeFacebookAppSecret", "http://example.com/auth/facebook/callback") + if err != nil { + t.Fatal(err) + } + runOAuth2IdentityTests(t, conn, tests) +}