@@ -13,6 +13,7 @@ const ( | |||
OT_GITHUB = iota + 1 | |||
OT_GOOGLE | |||
OT_TWITTER | |||
OT_QQ | |||
) | |||
var ( | |||
@@ -26,7 +27,7 @@ type Oauth2 struct { | |||
User *User `xorm:"-"` | |||
Type int `xorm:"unique(s) unique(oauth)"` // twitter,github,google... | |||
Identity string `xorm:"unique(s) unique(oauth)"` // id.. | |||
Token string `xorm:"VARCHAR(200) not null"` | |||
Token string `xorm:"TEXT not null"` | |||
} | |||
func BindUserOauth2(userId, oauthId int64) error { | |||
@@ -48,7 +49,7 @@ func GetOauth2(identity string) (oa *Oauth2, err error) { | |||
return | |||
} else if !isExist { | |||
return nil, ErrOauth2RecordNotExists | |||
} else if oa.Uid == 0 { | |||
} else if oa.Uid == -1 { | |||
return oa, ErrOauth2NotAssociatedWithUser | |||
} | |||
oa.User, err = GetUserById(oa.Uid) |
@@ -6,65 +6,32 @@ package user | |||
import ( | |||
"encoding/json" | |||
"net/http" | |||
"fmt" | |||
"net/url" | |||
"strconv" | |||
"strings" | |||
"code.google.com/p/goauth2/oauth" | |||
"github.com/go-martini/martini" | |||
"github.com/gogits/gogs/models" | |||
"github.com/gogits/gogs/modules/base" | |||
"github.com/gogits/gogs/modules/log" | |||
"github.com/gogits/gogs/modules/middleware" | |||
) | |||
type SocialConnector interface { | |||
Identity() string | |||
Name() string | |||
Email() string | |||
TokenString() string | |||
} | |||
type SocialGithub struct { | |||
data struct { | |||
Id int `json:"id"` | |||
Name string `json:"login"` | |||
Email string `json:"email"` | |||
} | |||
Token *oauth.Token | |||
type BasicUserInfo struct { | |||
Identity string | |||
Name string | |||
Email string | |||
} | |||
func (s *SocialGithub) Identity() string { | |||
return strconv.Itoa(s.data.Id) | |||
} | |||
func (s *SocialGithub) Name() string { | |||
return s.data.Name | |||
} | |||
func (s *SocialGithub) Email() string { | |||
return s.data.Email | |||
} | |||
func (s *SocialGithub) TokenString() string { | |||
data, _ := json.Marshal(s.Token) | |||
return string(data) | |||
} | |||
type SocialConnector interface { | |||
Type() int | |||
SetRedirectUrl(string) | |||
UserInfo(*oauth.Token, *url.URL) (*BasicUserInfo, error) | |||
// Github API refer: https://developer.github.com/v3/users/ | |||
func (s *SocialGithub) Update() error { | |||
scope := "https://api.github.com/user" | |||
transport := &oauth.Transport{ | |||
Token: s.Token, | |||
} | |||
log.Debug("update github info") | |||
r, err := transport.Client().Get(scope) | |||
if err != nil { | |||
return err | |||
} | |||
defer r.Body.Close() | |||
return json.NewDecoder(r.Body).Decode(&s.data) | |||
AuthCodeURL(string) string | |||
Exchange(string) (*oauth.Token, error) | |||
} | |||
func extractPath(next string) string { | |||
@@ -75,85 +42,76 @@ func extractPath(next string) string { | |||
return n.Path | |||
} | |||
// github && google && ... | |||
func SocialSignIn(ctx *middleware.Context) { | |||
//if base.OauthService != nil && base.OauthService.GitHub.Enabled { | |||
//} | |||
var ( | |||
SocialBaseUrl = "/user/login" | |||
SocialMap = make(map[string]SocialConnector) | |||
) | |||
var socid int64 | |||
var ok bool | |||
next := extractPath(ctx.Query("next")) | |||
log.Debug("social signed check %s", next) | |||
if socid, ok = ctx.Session.Get("socialId").(int64); ok && socid != 0 { | |||
// already login | |||
ctx.Redirect(next) | |||
log.Info("login soc id: %v", socid) | |||
// github && google && ... | |||
func SocialSignIn(params martini.Params, ctx *middleware.Context) { | |||
if base.OauthService == nil || !base.OauthService.GitHub.Enabled { | |||
ctx.Handle(404, "social login not enabled", nil) | |||
return | |||
} | |||
config := &oauth.Config{ | |||
ClientId: base.OauthService.GitHub.ClientId, | |||
ClientSecret: base.OauthService.GitHub.ClientSecret, | |||
RedirectURL: strings.TrimSuffix(base.AppUrl, "/") + ctx.Req.URL.RequestURI(), | |||
Scope: base.OauthService.GitHub.Scopes, | |||
AuthURL: "https://github.com/login/oauth/authorize", | |||
TokenURL: "https://github.com/login/oauth/access_token", | |||
} | |||
transport := &oauth.Transport{ | |||
Config: config, | |||
Transport: http.DefaultTransport, | |||
next := extractPath(ctx.Query("next")) | |||
name := params["name"] | |||
connect, ok := SocialMap[name] | |||
if !ok { | |||
ctx.Handle(404, "social login", nil) | |||
return | |||
} | |||
code := ctx.Query("code") | |||
if code == "" { | |||
// redirect to social login page | |||
ctx.Redirect(config.AuthCodeURL(next)) | |||
connect.SetRedirectUrl(strings.TrimSuffix(base.AppUrl, "/") + ctx.Req.URL.Host + ctx.Req.URL.Path) | |||
ctx.Redirect(connect.AuthCodeURL(next)) | |||
return | |||
} | |||
// handle call back | |||
tk, err := transport.Exchange(code) | |||
tk, err := connect.Exchange(code) // exchange for token | |||
if err != nil { | |||
log.Error("oauth2 handle callback error: %v", err) | |||
return // FIXME, need error page 501 | |||
ctx.Handle(500, "exchange code error", nil) | |||
return | |||
} | |||
next = extractPath(ctx.Query("state")) | |||
log.Debug("success token: %v", tk) | |||
log.Trace("success get token") | |||
gh := &SocialGithub{Token: tk} | |||
if err = gh.Update(); err != nil { | |||
// FIXME: handle error page 501 | |||
log.Error("connect with github error: %s", err) | |||
ui, err := connect.UserInfo(tk, ctx.Req.URL) | |||
if err != nil { | |||
ctx.Handle(500, fmt.Sprintf("get infomation from %s error: %v", name, err), nil) | |||
log.Error("social connect error: %s", err) | |||
return | |||
} | |||
var soc SocialConnector = gh | |||
log.Info("login: %s", soc.Name()) | |||
oa, err := models.GetOauth2(soc.Identity()) | |||
log.Info("social login: %s", ui) | |||
oa, err := models.GetOauth2(ui.Identity) | |||
switch err { | |||
case nil: | |||
ctx.Session.Set("userId", oa.User.Id) | |||
ctx.Session.Set("userName", oa.User.Name) | |||
case models.ErrOauth2RecordNotExists: | |||
oa = &models.Oauth2{} | |||
raw, _ := json.Marshal(tk) // json encode | |||
oa.Token = string(raw) | |||
oa.Uid = -1 | |||
oa.Type = models.OT_GITHUB | |||
oa.Token = soc.TokenString() | |||
oa.Identity = soc.Identity() | |||
log.Debug("oa: %v", oa) | |||
oa.Type = connect.Type() | |||
oa.Identity = ui.Identity | |||
log.Trace("oa: %v", oa) | |||
if err = models.AddOauth2(oa); err != nil { | |||
log.Error("add oauth2 %v", err) // 501 | |||
return | |||
} | |||
case models.ErrOauth2NotAssociatedWithUser: | |||
ctx.Session.Set("socialId", oa.Id) | |||
ctx.Session.Set("socialName", soc.Name()) | |||
ctx.Session.Set("socialEmail", soc.Email()) | |||
ctx.Redirect("/user/sign_up") | |||
return | |||
next = "/user/sign_up" | |||
default: | |||
log.Error(err.Error()) // FIXME: handle error page | |||
log.Error("other error: %v", err) | |||
ctx.Handle(500, err.Error(), nil) | |||
return | |||
} | |||
ctx.Session.Set("socialId", oa.Id) | |||
log.Debug("socialId: %v", oa.Id) | |||
ctx.Session.Set("socialName", ui.Name) | |||
ctx.Session.Set("socialEmail", ui.Email) | |||
log.Trace("socialId: %v", oa.Id) | |||
ctx.Redirect(next) | |||
} |
@@ -0,0 +1,73 @@ | |||
// Copyright 2014 The Gogs Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
package user | |||
import ( | |||
"encoding/json" | |||
"net/http" | |||
"net/url" | |||
"strconv" | |||
"strings" | |||
"code.google.com/p/goauth2/oauth" | |||
"github.com/gogits/gogs/models" | |||
"github.com/gogits/gogs/modules/base" | |||
) | |||
type SocialGithub struct { | |||
Token *oauth.Token | |||
*oauth.Transport | |||
} | |||
func (s *SocialGithub) Type() int { | |||
return models.OT_GITHUB | |||
} | |||
func init() { | |||
github := &SocialGithub{} | |||
name := "github" | |||
config := &oauth.Config{ | |||
ClientId: "09383403ff2dc16daaa1", //base.OauthService.GitHub.ClientId, // FIXME: panic when set | |||
ClientSecret: "0e4aa0c3630df396cdcea01a9d45cacf79925fea", //base.OauthService.GitHub.ClientSecret, | |||
RedirectURL: strings.TrimSuffix(base.AppUrl, "/") + "/user/login/" + name, //ctx.Req.URL.RequestURI(), | |||
Scope: "https://api.github.com/user", | |||
AuthURL: "https://github.com/login/oauth/authorize", | |||
TokenURL: "https://github.com/login/oauth/access_token", | |||
} | |||
github.Transport = &oauth.Transport{ | |||
Config: config, | |||
Transport: http.DefaultTransport, | |||
} | |||
SocialMap[name] = github | |||
} | |||
func (s *SocialGithub) SetRedirectUrl(url string) { | |||
s.Transport.Config.RedirectURL = url | |||
} | |||
func (s *SocialGithub) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) { | |||
transport := &oauth.Transport{ | |||
Token: token, | |||
} | |||
var data struct { | |||
Id int `json:"id"` | |||
Name string `json:"login"` | |||
Email string `json:"email"` | |||
} | |||
var err error | |||
r, err := transport.Client().Get(s.Transport.Scope) | |||
if err != nil { | |||
return nil, err | |||
} | |||
defer r.Body.Close() | |||
if err = json.NewDecoder(r.Body).Decode(&data); err != nil { | |||
return nil, err | |||
} | |||
return &BasicUserInfo{ | |||
Identity: strconv.Itoa(data.Id), | |||
Name: data.Name, | |||
Email: data.Email, | |||
}, nil | |||
} |
@@ -0,0 +1,71 @@ | |||
// Copyright 2014 The Gogs Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
package user | |||
import ( | |||
"encoding/json" | |||
"net/http" | |||
"net/url" | |||
"github.com/gogits/gogs/models" | |||
"code.google.com/p/goauth2/oauth" | |||
) | |||
type SocialGoogle struct { | |||
Token *oauth.Token | |||
*oauth.Transport | |||
} | |||
func (s *SocialGoogle) Type() int { | |||
return models.OT_GOOGLE | |||
} | |||
func init() { | |||
google := &SocialGoogle{} | |||
name := "google" | |||
// get client id and secret from | |||
// https://console.developers.google.com/project | |||
config := &oauth.Config{ | |||
ClientId: "849753812404-mpd7ilvlb8c7213qn6bre6p6djjskti9.apps.googleusercontent.com", //base.OauthService.GitHub.ClientId, // FIXME: panic when set | |||
ClientSecret: "VukKc4MwaJUSmiyv3D7ANVCa", //base.OauthService.GitHub.ClientSecret, | |||
Scope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile", | |||
AuthURL: "https://accounts.google.com/o/oauth2/auth", | |||
TokenURL: "https://accounts.google.com/o/oauth2/token", | |||
} | |||
google.Transport = &oauth.Transport{ | |||
Config: config, | |||
Transport: http.DefaultTransport, | |||
} | |||
SocialMap[name] = google | |||
} | |||
func (s *SocialGoogle) SetRedirectUrl(url string) { | |||
s.Transport.Config.RedirectURL = url | |||
} | |||
func (s *SocialGoogle) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) { | |||
transport := &oauth.Transport{Token: token} | |||
var data struct { | |||
Id string `json:"id"` | |||
Name string `json:"name"` | |||
Email string `json:"email"` | |||
} | |||
var err error | |||
reqUrl := "https://www.googleapis.com/oauth2/v1/userinfo" | |||
r, err := transport.Client().Get(reqUrl) | |||
if err != nil { | |||
return nil, err | |||
} | |||
defer r.Body.Close() | |||
if err = json.NewDecoder(r.Body).Decode(&data); err != nil { | |||
return nil, err | |||
} | |||
return &BasicUserInfo{ | |||
Identity: data.Id, | |||
Name: data.Name, | |||
Email: data.Email, | |||
}, nil | |||
} |
@@ -0,0 +1,83 @@ | |||
// Copyright 2014 The Gogs Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
// api reference: http://wiki.open.t.qq.com/index.php/OAuth2.0%E9%89%B4%E6%9D%83/Authorization_code%E6%8E%88%E6%9D%83%E6%A1%88%E4%BE%8B | |||
package user | |||
import ( | |||
"encoding/json" | |||
"net/http" | |||
"net/url" | |||
"github.com/gogits/gogs/models" | |||
"code.google.com/p/goauth2/oauth" | |||
) | |||
type SocialQQ struct { | |||
Token *oauth.Token | |||
*oauth.Transport | |||
reqUrl string | |||
} | |||
func (s *SocialQQ) Type() int { | |||
return models.OT_QQ | |||
} | |||
func init() { | |||
qq := &SocialQQ{} | |||
name := "qq" | |||
config := &oauth.Config{ | |||
ClientId: "801497180", //base.OauthService.GitHub.ClientId, // FIXME: panic when set | |||
ClientSecret: "16cd53b8ad2e16a36fc2c8f87d9388f2", //base.OauthService.GitHub.ClientSecret, | |||
Scope: "all", | |||
AuthURL: "https://open.t.qq.com/cgi-bin/oauth2/authorize", | |||
TokenURL: "https://open.t.qq.com/cgi-bin/oauth2/access_token", | |||
} | |||
qq.reqUrl = "https://open.t.qq.com/api/user/info" | |||
qq.Transport = &oauth.Transport{ | |||
Config: config, | |||
Transport: http.DefaultTransport, | |||
} | |||
SocialMap[name] = qq | |||
} | |||
func (s *SocialQQ) SetRedirectUrl(url string) { | |||
s.Transport.Config.RedirectURL = url | |||
} | |||
func (s *SocialQQ) UserInfo(token *oauth.Token, URL *url.URL) (*BasicUserInfo, error) { | |||
var data struct { | |||
Data struct { | |||
Id string `json:"openid"` | |||
Name string `json:"name"` | |||
Email string `json:"email"` | |||
} `json:"data"` | |||
} | |||
var err error | |||
// https://open.t.qq.com/api/user/info? | |||
//oauth_consumer_key=APP_KEY& | |||
//access_token=ACCESSTOKEN&openid=openid | |||
//clientip=CLIENTIP&oauth_version=2.a | |||
//scope=all | |||
var urls = url.Values{ | |||
"oauth_consumer_key": {s.Transport.Config.ClientId}, | |||
"access_token": {token.AccessToken}, | |||
"openid": URL.Query()["openid"], | |||
"oauth_version": {"2.a"}, | |||
"scope": {"all"}, | |||
} | |||
r, err := http.Get(s.reqUrl + "?" + urls.Encode()) | |||
if err != nil { | |||
return nil, err | |||
} | |||
defer r.Body.Close() | |||
if err = json.NewDecoder(r.Body).Decode(&data); err != nil { | |||
return nil, err | |||
} | |||
return &BasicUserInfo{ | |||
Identity: data.Data.Id, | |||
Name: data.Data.Name, | |||
Email: data.Data.Email, | |||
}, nil | |||
} |
@@ -88,7 +88,7 @@ func runWeb(*cli.Context) { | |||
m.Group("/user", func(r martini.Router) { | |||
r.Get("/login", user.SignIn) | |||
r.Post("/login", bindIgnErr(auth.LogInForm{}), user.SignInPost) | |||
r.Get("/login/github", user.SocialSignIn) | |||
r.Get("/login/:name", user.SocialSignIn) | |||
r.Get("/sign_up", user.SignUp) | |||
r.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost) | |||
r.Get("/reset_password", user.ResetPasswd) |