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 4fe9c1b9bddcd73193592c80e748384a46310e77
parent 33a9bae99e60665d0d977904927ffbd9990b0a14
Author: Sebastian Mancke <s.mancke@tarent.de>
Date:   Tue, 14 Nov 2017 16:34:45 +0100

Merge pull request #55 from afdecastro879/master

#51 | Added Bitbucket Oauth2 support
Diffstat:
MREADME.md | 4+++-
Aoauth2/bitbucket.go | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aoauth2/bitbucket_test.go | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Moauth2/provider_test.go | 7++++++-
4 files changed, 272 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md @@ -49,7 +49,8 @@ _Note for Caddy users_: Not all parameters are available in Caddy. See the table | -cookie-http-only | boolean | true | X | Set the cookie with the http only flag | | -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=..] | +| -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=..] | | -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 | @@ -241,6 +242,7 @@ Currently the following oauth Provider is supported: * github * google +* bitbucket An Oauth Provider supports the following parameters: diff --git a/oauth2/bitbucket.go b/oauth2/bitbucket.go @@ -0,0 +1,132 @@ +package oauth2 + +import ( + "encoding/json" + "fmt" + "github.com/tarent/loginsrv/model" + "io/ioutil" + "net/http" + "strings" +) + +var bitbucketAPI = "https://api.bitbucket.org/2.0" + +// Using the avatar url to be able to fetch 128px image. By default BitbucketAPI return 32px image. +var bitbucketAvatarURL = "https://bitbucket.org/account/%v/avatar/128/" + +func init() { + RegisterProvider(providerBitbucket) +} + +// bitbucketUser is used for parsing the github response +type bitbucketUser struct { + Username string `json:"username,omitempty"` + DisplayName string `json:"display_name,omitempty"` + Email string `json:"email,omitempty"` +} + +// emails is used to parse user email information +type emails struct { + Page int `json:"page,omitempty"` + PageLen int `json:"pagelen,omitempty"` + Size int `json:"size,omitempty"` + Values []email +} + +// email used to parse one user's email +type email struct { + Email string `json:"email,omitempty"` + IsConfirmed bool `json:"is_confirmed,omitempty"` + IsPrimary bool `json:"is_primary,omitempty"` + Links struct { + Self struct { + Href string + } + } `json:"links,omitempty"` + Type string `json:"type,omitempty"` +} + +// getPrimaryEmailAddress retrieve the primary email address of the user +func (e *emails) getPrimaryEmailAddress() string { + for _, val := range e.Values { + if val.IsPrimary { + return val.Email + } + } + return "" +} + +// getBitbucketEmails Retrieves bitbucket user emails from the Bitbucket API emails service +func getBitbucketEmails(token TokenInfo) (emails, error) { + emailUrl := fmt.Sprintf("%v/user/emails?access_token=%v", bitbucketAPI, token.AccessToken) + userEmails := emails{} + resp, err := http.Get(emailUrl) + + if err != nil { + return emails{}, err + } + + if !strings.Contains(resp.Header.Get("Content-Type"), "application/json") { + return emails{}, fmt.Errorf("wrong content-type on bitbucket get user emails: %v", resp.Header.Get("Content-Type")) + } + + if resp.StatusCode != 200 { + return emails{}, fmt.Errorf("got http status %v on bitbucket get user emails", resp.StatusCode) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return emails{}, fmt.Errorf("error reading bitbucket get user emails: %v", err) + } + + err = json.Unmarshal(b, &userEmails) + + if err != nil { + return emails{}, fmt.Errorf("error parsing bitbucket get user emails: %v", err) + } + + return userEmails, nil +} + +var providerBitbucket = Provider{ + Name: "bitbucket", + AuthURL: "https://bitbucket.org/site/oauth2/authorize", + TokenURL: "https://bitbucket.org/site/oauth2/access_token", + GetUserInfo: func(token TokenInfo) (model.UserInfo, string, error) { + gu := bitbucketUser{} + url := fmt.Sprintf("%v/user?access_token=%v", bitbucketAPI, token.AccessToken) + resp, err := http.Get(url) + if err != nil { + return model.UserInfo{}, "", err + } + + if !strings.Contains(resp.Header.Get("Content-Type"), "application/json") { + return model.UserInfo{}, "", fmt.Errorf("wrong content-type on bitbucket get user info: %v", resp.Header.Get("Content-Type")) + } + + if resp.StatusCode != 200 { + return model.UserInfo{}, "", fmt.Errorf("got http status %v on bitbucket get user info", resp.StatusCode) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return model.UserInfo{}, "", fmt.Errorf("error reading bitbucket get user info: %v", err) + } + + err = json.Unmarshal(b, &gu) + + if err != nil { + return model.UserInfo{}, "", fmt.Errorf("error parsing bitbucket get user info: %v", err) + } + + userEmails, err := getBitbucketEmails(token) + + return model.UserInfo{ + Sub: gu.Username, + Picture: fmt.Sprintf(bitbucketAvatarURL, gu.Username), + Name: gu.DisplayName, + Email: userEmails.getPrimaryEmailAddress(), + Origin: "bitbucket", + }, string(b), nil + }, +} diff --git a/oauth2/bitbucket_test.go b/oauth2/bitbucket_test.go @@ -0,0 +1,131 @@ +package oauth2 + +import ( + "net/http" + "net/http/httptest" + "testing" + "github.com/gorilla/mux" + "github.com/stretchr/testify/suite" + "encoding/json" +) + +var bitbucketTestUserResponse = `{ + "created_on": "2011-12-20T16:34:07.132459+00:00", + "display_name": "tutorials account", + "is_staff": false, + "links": { + "avatar": { + "href": "https://bitbucket.org/account/tutorials/avatar/32/" + }, + "followers": { + "href": "https://api.bitbucket.org/2.0/users/tutorials/followers" + }, + "following": { + "href": "https://api.bitbucket.org/2.0/users/tutorials/following" + }, + "hooks": { + "href": "https://api.bitbucket.org/2.0/users/tutorials/hooks" + }, + "html": { + "href": "https://bitbucket.org/tutorials/" + }, + "repositories": { + "href": "https://api.bitbucket.org/2.0/repositories/tutorials" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/tutorials" + }, + "snippets": { + "href": "https://api.bitbucket.org/2.0/snippets/tutorials" + } + }, + "location": null, + "type": "user", + "username": "tutorials", + "uuid": "{c788b2da-b7a2-404c-9e26-d3f077557007}", + "website": "https://tutorials.bitbucket.org/" +}` + +var bitbucketTestUserEmailResponse = `{ + "page": 1, + "pagelen": 10, + "size": 1, + "values": [ + { + "email": "tutorials@bitbucket.com", + "is_confirmed": true, + "is_primary": true, + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/user/emails/tutorials@bitbucket.com" + } + }, + "type": "email" + }, + { + "email": "anotheremail@bitbucket.com", + "is_confirmed": false, + "is_primary": false, + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/user/emails/anotheremail@bitbucket.com" + } + }, + "type": "email" + } + ] +}` + +// BitbucketTestSuite Model for the bitbucket test suite +type BitbucketTestSuite struct { + suite.Suite + Server *httptest.Server +} + +// SetupTest a method that will be run before any method of this suite. It setups a mock server for bitbucket API +func (suite *BitbucketTestSuite) SetupTest() { + r := mux.NewRouter() + + userHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + suite.Equal("secret", r.FormValue("access_token")) + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Write([]byte(bitbucketTestUserResponse)) + }) + + emailHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + suite.Equal("secret", r.FormValue("access_token")) + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Write([]byte(bitbucketTestUserEmailResponse)) + }) + + r.HandleFunc("/user", userHandler) + r.HandleFunc("/user/emails", emailHandler) + + suite.Server = httptest.NewServer(r) +} + +// Test_Bitbucket_getUserInfo Tests Bitbucket provider returns the expected information +func (suite *BitbucketTestSuite) Test_Bitbucket_getUserInfo() { + + bitbucketAPI = suite.Server.URL + + u, rawJSON, err := providerBitbucket.GetUserInfo(TokenInfo{AccessToken: "secret"}) + suite.NoError(err) + suite.Equal("tutorials", u.Sub) + suite.Equal("tutorials@bitbucket.com", u.Email) + suite.Equal("tutorials account", u.Name) + suite.Equal(bitbucketTestUserResponse, rawJSON) +} + +// Test_Bitbucket_getPrimaryEmailAddress Tests the returned primary email is the expected email +func (suite *BitbucketTestSuite) Test_Bitbucket_getPrimaryEmailAddress() { + userEmails := emails{} + err := json.Unmarshal([]byte(bitbucketTestUserEmailResponse), &userEmails) + suite.NoError(err) + suite.Equal("tutorials@bitbucket.com", userEmails.getPrimaryEmailAddress()) +} + +// Test_Bitbucket_Suite Runs the entire suite for Bitbucket +func Test_Bitbucket_Suite(t *testing.T) { + suite.Run(t, new(BitbucketTestSuite)) +} diff --git a/oauth2/provider_test.go b/oauth2/provider_test.go @@ -14,8 +14,13 @@ func Test_ProviderRegistration(t *testing.T) { NotNil(t, google) True(t, exist) + bitbucket, exist := GetProvider("bitbucket") + NotNil(t, bitbucket) + True(t, exist) + list := ProviderList() - Equal(t, 2, len(list)) + Equal(t, 3, len(list)) Contains(t, list, "github") Contains(t, list, "google") + Contains(t, list, "bitbucket") }