Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add gitee provider #525

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
230 changes: 230 additions & 0 deletions providers/gitee/gitee.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// Package gitee implements the OAuth2 protocol for authenticating users through Gitee.
// This package can be used as a reference implementation of an OAuth2 provider for Goth.
package gitee

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"

"github.com/markbates/goth"
"golang.org/x/oauth2"
)

const providerName = "gitee"

var (
AuthURL = "https://gitee.com/oauth/authorize"
TokenURL = "https://gitee.com/oauth/token"
ProfileURL = "https://gitee.com/api/v5/user"
EmailURL = "https://gitee.com/api/v5/emails"
)

var ErrNoPrimaryEmail = errors.New("The user does not have a primary email on Gitee")

type Provider struct {
Key string
Secret string
CallbackURL string
HTTPClient *http.Client
config *oauth2.Config
providerName string
profileURL string
emailURL string
}

// Name is the name used to retrieve this provider later.
func (p *Provider) Name() string {
return p.providerName
}

// SetName is to update the name of the provider (needed in case of multiple providers of 1 type)
func (p *Provider) SetName(name string) {
p.providerName = name
}

func (p *Provider) Client() *http.Client {
return goth.HTTPClientWithFallBack(p.HTTPClient)
}

// Debug is a no-op for the package.
func (p *Provider) Debug(debug bool) {
// todo:for debug log?
}

// BeginAuth asks Gitee for an authentication endpoint.
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
authURL := p.config.AuthCodeURL(state)
session := &Session{
AuthURL: authURL,
}
return session, nil
}

func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
user := goth.User{
Provider: p.Name(),
}

sess, ok := session.(*Session)
if !ok {
return user, errors.New("invalid session assert")
}
if sess.AccessToken == "" {
// data is not yet retrieved since accessToken is still empty
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
}
user.AccessToken = sess.AccessToken

req, err := http.NewRequest(http.MethodGet, p.profileURL, nil)
if err != nil {
return user, nil
}

req.Header.Set("Authorization", "Bearer "+sess.AccessToken)
rsp, err := p.Client().Do(req)
if err != nil {
return user, nil
}
defer rsp.Body.Close()

if rsp.StatusCode != http.StatusOK {
return user, fmt.Errorf("gitee API responded with a %d trying to fetch user information", rsp.StatusCode)
}

err = parseUserFromBody(rsp.Body, &user)
if err != nil {
return user, err
}

if user.Email == "" {
for _, scope := range p.config.Scopes {
if strings.TrimSpace(scope) == "user" || strings.TrimSpace(scope) == "emails" {
user.Email, err = getPrivateMail(p, sess)
if err != nil {
return user, err
}
break
}
}
}

return user, nil
}

func parseUserFromBody(r io.Reader, user *goth.User) error {
err := json.NewDecoder(r).Decode(&user.RawData)
if err != nil {
fmt.Printf("x2:%+v", err)
return err
}

if login, ok := user.RawData["login"].(string); ok {
user.Name = login
}
if name, ok := user.RawData["name"].(string); ok {
user.NickName = name
}
if email, ok := user.RawData["email"].(string); ok {
user.Email = email
}
if bio, ok := user.RawData["bio"].(string); ok {
user.Description = bio
}
if avatarURL, ok := user.RawData["avatar_url"].(string); ok {
user.AvatarURL = avatarURL
}

return nil
}

type emails []struct {
Email string `json:"email"`
State string `json:"state"`
Scope []string `json:"scope"`
}

func getPrivateMail(p *Provider, sess *Session) (email string, err error) {
req, err := http.NewRequest(http.MethodGet, p.emailURL, nil)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json;charset=UTF-8")
req.Header.Set("Authorization", "Bearer "+sess.AccessToken)

rsp, err := p.Client().Do(req)
if err != nil {
return "", err
}
defer rsp.Body.Close()

if rsp.StatusCode != http.StatusOK {
return "", fmt.Errorf("gitee API responded with a %d trying to get user email", rsp.StatusCode)
}

var emails emails
err = json.NewDecoder(rsp.Body).Decode(&emails)
if err != nil {
return "", err
}

for _, email := range emails {
for _, scope := range email.Scope {
if scope == "primary" {
return email.Email, nil
}
}
}

return "", ErrNoPrimaryEmail
}

// RefreshToken refresh token is provided by Gitee
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
return nil, errors.New("todo: Refresh token is not provided by Gitee")
}

func (p *Provider) RefreshTokenAvailable() bool {
return false
}

func New(key, secret, callbackURL string, scopes ...string) *Provider {
return NewCustomisedURL(key, secret, callbackURL, AuthURL, TokenURL, ProfileURL, EmailURL, scopes...)
}

func NewCustomisedURL(key, secret, callbackURL, authURL, tokenURL, profileURL, emailURL string, scopes ...string) *Provider {
p := &Provider{
Key: key,
Secret: secret,
CallbackURL: callbackURL,
HTTPClient: &http.Client{},
config: &oauth2.Config{},
providerName: providerName,
profileURL: ProfileURL,
emailURL: EmailURL,
}
p.config = newConfig(p, authURL, tokenURL, scopes)
return p
}

func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config {
c := &oauth2.Config{
ClientID: provider.Key,
ClientSecret: provider.Secret,
Endpoint: oauth2.Endpoint{
AuthURL: authURL,
TokenURL: tokenURL,
},
RedirectURL: provider.CallbackURL,
Scopes: []string{},
}

for _, scope := range scopes {
c.Scopes = append(c.Scopes, scope)
}

return c
}
92 changes: 92 additions & 0 deletions providers/gitee/gitee_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package gitee

import (
"fmt"
"os"
"strings"
"testing"

"github.com/markbates/goth"
"github.com/stretchr/testify/assert"
)

func newProvider() *Provider {
return New(os.Getenv("GITEE_KEY"), os.Getenv("GITEE_SECRET"), "/foo", "user")
}

func newCustomisedProvider() *Provider {
return NewCustomisedURL(os.Getenv("GITEE_KEY"), os.Getenv("GITEE_SECRET"), "/foo", "http://authURL", "http://tokenURL", "http://profileURL", "http://emailURL")
}

func Test_New(t *testing.T) {
t.Parallel()
a := assert.New(t)

provider := newProvider()
a.Equal(provider.Key, os.Getenv("GITEE_KEY"))
a.Equal(provider.Secret, os.Getenv("GITEE_SECRET"))
a.Equal(provider.CallbackURL, "/foo")
}

func Test_NewCustomisedURL(t *testing.T) {
t.Parallel()
a := assert.New(t)
p := newCustomisedProvider()
sess, err := p.BeginAuth("state")
a.NoError(err)

authURL, err := sess.GetAuthURL()
a.NoError(err)
a.Contains(authURL, "http://authURL")
}

func TestImplementProvider(t *testing.T) {
t.Parallel()
a := assert.New(t)
a.Implements((*goth.Provider)(nil), newProvider())
}

func TestBeginAuth(t *testing.T) {
t.Parallel()
a := assert.New(t)

provider := newProvider()
sess, err := provider.BeginAuth("state")
a.NoError(err)

authURL, err := sess.GetAuthURL()
a.NoError(err)
a.Contains(authURL, "gitee.com/oauth/authorize")
a.Contains(authURL, fmt.Sprintf("client_id=%s", os.Getenv("GITEE_KEY")))
a.Contains(authURL, "state=state")
a.Contains(authURL, "scope=user")
}

func TestSessionFromJSON(t *testing.T) {
t.Parallel()
a := assert.New(t)
provider := newProvider()
sess, err := provider.UnmarshalSession(`{"AuthURL":"http://gitee.com/auth_url","AccessToken":"01234567890"}`)
a.NoError(err)
authURL, err := sess.GetAuthURL()
a.NoError(err)

a.Equal(authURL, "http://gitee.com/auth_url")
}

func Test_parse(t *testing.T) {
a := assert.New(t)
s := `
{
"id": 123456,
"login": "login_name",
"name": "name",
"bio": "some bio",
"email": ""
}
`
r := strings.NewReader(s)
user := &goth.User{}
err := parseUserFromBody(r, user)
a.NoError(err)
}
52 changes: 52 additions & 0 deletions providers/gitee/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package gitee

import (
"encoding/json"
"errors"
"strings"

"github.com/markbates/goth"
)

type Session struct {
AuthURL string
AccessToken string
}

func (s *Session) GetAuthURL() (string, error) {
if s.AuthURL == "" {
return "", errors.New(goth.NoAuthUrlErrorMessage)
}
return s.AuthURL, nil
}

func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) {
p := provider.(*Provider)
token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code"))
if err != nil {
return "", err
}

if !token.Valid() {
return "", errors.New("Invalid token received from provider")
}

s.AccessToken = token.AccessToken
return token.AccessToken, err
}

// Marshal the session into a string
func (s *Session) Marshal() string {
b, _ := json.Marshal(s)
return string(b)
}

func (s *Session) String() string {
return s.Marshal()
}

func (p *Provider) UnmarshalSession(data string) (goth.Session, error) {
sess := &Session{}
err := json.NewDecoder(strings.NewReader(data)).Decode(sess)
return sess, err
}
1 change: 1 addition & 0 deletions providers/gitee/session_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package gitee