Merge pull request #55 from afdecastro879/master

#51 | Added Bitbucket Oauth2 support
This commit is contained in:
Sebastian Mancke
2017-11-14 16:34:45 +01:00
committed by GitHub
4 changed files with 272 additions and 2 deletions
+3 -1
View File
@@ -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:
+132
View File
@@ -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
},
}
+131
View File
@@ -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))
}
+6 -1
View File
@@ -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")
}