commit af9d05217a4986aa63a5744018316ee6a56fafa7
parent 40a5fceb6603c7fb19a777709a2fa27f748d23f6
Author: Sebastian Mancke <s.mancke@tarent.de>
Date: Tue, 20 Feb 2018 07:15:48 +0100
Merge pull request #69 from afdecastro879/master
Facebook OAuth provider
Diffstat:
4 files changed, 158 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
@@ -31,6 +31,7 @@ The following providers (login backends) are supported.
* Github Login
* Google Login
* Bitbucket Login
+ * Facebook Login
## Questions
@@ -51,6 +52,8 @@ _Note for Caddy users_: Not all parameters are available in Caddy. See the table
| -cookie-name | string | "jwt_token" | X | The name of the jwt cookie |
| -github | value | | X | Oauth config in the form: client_id=..,client_secret=..[,scope=..,][redirect_uri=..] |
| -google | value | | X | Oauth config in the form: client_id=..,client_secret=..,scope=..[redirect_uri=..] |
+| -bitbucket | value | | X | Oauth config in the form: client_id=..,client_secret=..,[,scope=..][redirect_uri=..] |
+| -facebook | value | | X | Oauth config in the form: client_id=..,client_secret=..,scope=email..[redirect_uri=..] |
| -host | string | "localhost" | - | The host to listen on |
| -htpasswd | value | | X | Htpasswd login backend opts: file=/path/to/pwdfile |
| -jwt-expiry | go duration | 24h | X | The expiry duration for the jwt token, e.g. 2h or 3h30m |
@@ -262,6 +265,7 @@ Currently the following oauth Provider is supported:
* github
* google (see a note below)
* bitbucket
+* facebook (see a note below)
An Oauth Provider supports the following parameters:
@@ -285,6 +289,10 @@ $ docker run -p 80:80 tarent/loginsrv -github client_id=xxx,client_secret=yyy
You can use `scope=https://www.googleapis.com/auth/userinfo.email`. When configuring OAuth 2 credentials in Google Cloud Console, don't forget to enable corresponding API's.
For example, for `scope=https://www.googleapis.com/auth/userinfo.profile` [Google People API](https://console.cloud.google.com/apis/library/people.googleapis.com/) must be enabled for your project. Keep in mind that it usually takes a few minutes for this setting to take effect.
+### Note for Facebbok's Oauth 2
+Make sure you ask for the scope `email` when adding your facebook config option. Otherwise the provider should not be able to fetch
+the user's email.
+
## Templating
A custom template can be supplied by the parameter `template`.
diff --git a/oauth2/facebook.go b/oauth2/facebook.go
@@ -0,0 +1,76 @@
+package oauth2
+
+import (
+ "github.com/tarent/loginsrv/model"
+ "fmt"
+ "net/http"
+ "io/ioutil"
+ "encoding/json"
+ "strings"
+)
+
+var facebookAPI = "https://graph.facebook.com/v2.12"
+
+func init() {
+ RegisterProvider(providerfacebook)
+}
+
+// facebookUser is used for parsing the facebook response
+type facebookUser struct {
+ UserID string `json:"id,omitempty"`
+ Picture struct{
+ Data struct{
+ URL string `json:"url,omitempty"`
+ } `json:"data,omitempty"`
+ } `json:"picture,omitempty"`
+ Name string `json:"name,omitempty"`
+ Email string `json:"email,omitempty"`
+}
+
+var providerfacebook = Provider{
+ Name: "facebook",
+ AuthURL: "https://www.facebook.com/v2.12/dialog/oauth",
+ TokenURL: "https://graph.facebook.com/v2.12/oauth/access_token",
+ GetUserInfo: func(token TokenInfo) (model.UserInfo, string, error) {
+ fu := facebookUser{}
+
+ url := fmt.Sprintf("%v/me?access_token=%v&fields=name,email,id,picture", facebookAPI, token.AccessToken)
+
+ // For facebook return an application/json Content-type the Accept header should be set as 'application/json'
+ client := &http.Client{}
+ contentType := "application/json"
+ req, _ := http.NewRequest("GET", url, nil)
+ req.Header.Set("Accept", contentType)
+ resp, err := client.Do(req)
+
+ if err != nil {
+ return model.UserInfo{}, "", err
+ }
+
+ if !strings.Contains(resp.Header.Get("Content-Type"), contentType) {
+ return model.UserInfo{}, "", fmt.Errorf("wrong content-type on facebook get user info: %v", resp.Header.Get("Content-Type"))
+ }
+
+ if resp.StatusCode != 200 {
+ return model.UserInfo{}, "", fmt.Errorf("got http status %v on facebook get user info", resp.StatusCode)
+ }
+
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return model.UserInfo{}, "", fmt.Errorf("error reading facebook get user info: %v", err)
+ }
+
+ err = json.Unmarshal(b, &fu)
+ if err != nil {
+ return model.UserInfo{}, "", fmt.Errorf("error parsing facebook get user info: %v", err)
+ }
+
+ return model.UserInfo{
+ Sub: fu.UserID,
+ Picture: fu.Picture.Data.URL,
+ Name: fu.Name,
+ Email: fu.Email,
+ Origin: "facebook",
+ }, string(b), nil
+ },
+}
diff --git a/oauth2/facebook_test.go b/oauth2/facebook_test.go
@@ -0,0 +1,68 @@
+package oauth2
+
+import (
+ . "github.com/stretchr/testify/assert"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+var facebookTestUserResponse = `{
+ "id": "23456789012345678",
+ "name": "Facebook User",
+ "picture": {
+ "data": {
+ "height": 100,
+ "is_silhouette": false,
+ "url": "https://scontent.xx.fbcdn.net/v/t1.0-1/p100x100/example_facebook_image.jpg",
+ "width": 100
+ }
+ },
+ "email": "facebookuser@facebook.com"
+}`
+
+func Test_Facebook_getUserInfo(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ Equal(t, "secret", r.FormValue("access_token"))
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.Write([]byte(facebookTestUserResponse))
+ }))
+ defer server.Close()
+
+ facebookAPI = server.URL
+
+ u, rawJSON, err := providerfacebook.GetUserInfo(TokenInfo{AccessToken: "secret"})
+ NoError(t, err)
+ Equal(t, "23456789012345678", u.Sub)
+ Equal(t, "facebookuser@facebook.com", u.Email)
+ Equal(t, "Facebook User", u.Name)
+ Equal(t, facebookTestUserResponse, rawJSON)
+}
+
+func Test_Facebook_getUserInfo_WrongContentType(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ Equal(t, "secret", r.FormValue("access_token"))
+ w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
+ w.Write([]byte(facebookTestUserResponse))
+ }))
+ defer server.Close()
+
+ facebookAPI = server.URL
+
+ _, _, err := providerfacebook.GetUserInfo(TokenInfo{AccessToken: "secret"})
+ Error(t, err)
+}
+
+func Test_Facebook_getUserInfo_WrongStatus(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ Equal(t, "secret", r.FormValue("access_token"))
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte(facebookTestUserResponse))
+ }))
+ defer server.Close()
+
+ facebookAPI = server.URL
+
+ _, _, err := providerfacebook.GetUserInfo(TokenInfo{AccessToken: "secret"})
+ Error(t, err)
+}
diff --git a/oauth2/provider_test.go b/oauth2/provider_test.go
@@ -18,9 +18,14 @@ func Test_ProviderRegistration(t *testing.T) {
NotNil(t, bitbucket)
True(t, exist)
+ facebook, exist := GetProvider("facebook")
+ NotNil(t, facebook)
+ True(t, exist)
+
list := ProviderList()
- Equal(t, 3, len(list))
+ Equal(t, 4, len(list))
Contains(t, list, "github")
Contains(t, list, "google")
Contains(t, list, "bitbucket")
+ Contains(t, list, "facebook")
}