Connector: Connector for Facebook
This implements dex connector for facebook Fixes #391
This commit is contained in:
parent
9a78dca137
commit
464b7fb1af
3 changed files with 265 additions and 0 deletions
|
@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
148
connector/connector_facebook.go
Normal file
148
connector/connector_facebook.go
Normal file
|
@ -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
|
||||
}
|
71
connector/connector_facebook_test.go
Normal file
71
connector/connector_facebook_test.go
Normal file
|
@ -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)
|
||||
}
|
Reference in a new issue