mirror of
https://github.com/wahyd4/loginsrv.git
synced 2026-08-09 04:46:29 +10:00
Merge pull request #55 from afdecastro879/master
#51 | Added Bitbucket Oauth2 support
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user