loginsrv

Unnamed repository; edit this file 'description' to name the repository.
git clone git@jamesshield.xyz:repos/loginsrv.git
Log | Files | Refs | README | LICENSE

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:
MREADME.md | 8++++++++
Aoauth2/facebook.go | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aoauth2/facebook_test.go | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Moauth2/provider_test.go | 7++++++-
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") }