@@ -13,6 +13,7 @@ const ( | |||||
OT_GITHUB = iota + 1 | OT_GITHUB = iota + 1 | ||||
OT_GOOGLE | OT_GOOGLE | ||||
OT_TWITTER | OT_TWITTER | ||||
OT_QQ | |||||
) | ) | ||||
var ( | var ( | ||||
@@ -26,7 +27,7 @@ type Oauth2 struct { | |||||
User *User `xorm:"-"` | User *User `xorm:"-"` | ||||
Type int `xorm:"unique(s) unique(oauth)"` // twitter,github,google... | Type int `xorm:"unique(s) unique(oauth)"` // twitter,github,google... | ||||
Identity string `xorm:"unique(s) unique(oauth)"` // id.. | 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 { | func BindUserOauth2(userId, oauthId int64) error { | ||||
@@ -48,7 +49,7 @@ func GetOauth2(identity string) (oa *Oauth2, err error) { | |||||
return | return | ||||
} else if !isExist { | } else if !isExist { | ||||
return nil, ErrOauth2RecordNotExists | return nil, ErrOauth2RecordNotExists | ||||
} else if oa.Uid == 0 { | |||||
} else if oa.Uid == -1 { | |||||
return oa, ErrOauth2NotAssociatedWithUser | return oa, ErrOauth2NotAssociatedWithUser | ||||
} | } | ||||
oa.User, err = GetUserById(oa.Uid) | oa.User, err = GetUserById(oa.Uid) | ||||
@@ -6,65 +6,32 @@ package user | |||||
import ( | import ( | ||||
"encoding/json" | "encoding/json" | ||||
"net/http" | |||||
"fmt" | |||||
"net/url" | "net/url" | ||||
"strconv" | |||||
"strings" | "strings" | ||||
"code.google.com/p/goauth2/oauth" | "code.google.com/p/goauth2/oauth" | ||||
"github.com/go-martini/martini" | |||||
"github.com/gogits/gogs/models" | "github.com/gogits/gogs/models" | ||||
"github.com/gogits/gogs/modules/base" | "github.com/gogits/gogs/modules/base" | ||||
"github.com/gogits/gogs/modules/log" | "github.com/gogits/gogs/modules/log" | ||||
"github.com/gogits/gogs/modules/middleware" | "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 { | func extractPath(next string) string { | ||||
@@ -75,85 +42,76 @@ func extractPath(next string) string { | |||||
return n.Path | 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 | 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") | code := ctx.Query("code") | ||||
if code == "" { | if code == "" { | ||||
// redirect to social login page | // 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 | return | ||||
} | } | ||||
// handle call back | // handle call back | ||||
tk, err := transport.Exchange(code) | |||||
tk, err := connect.Exchange(code) // exchange for token | |||||
if err != nil { | if err != nil { | ||||
log.Error("oauth2 handle callback error: %v", err) | 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")) | 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 | 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 { | switch err { | ||||
case nil: | case nil: | ||||
ctx.Session.Set("userId", oa.User.Id) | ctx.Session.Set("userId", oa.User.Id) | ||||
ctx.Session.Set("userName", oa.User.Name) | ctx.Session.Set("userName", oa.User.Name) | ||||
case models.ErrOauth2RecordNotExists: | case models.ErrOauth2RecordNotExists: | ||||
oa = &models.Oauth2{} | oa = &models.Oauth2{} | ||||
raw, _ := json.Marshal(tk) // json encode | |||||
oa.Token = string(raw) | |||||
oa.Uid = -1 | 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 { | if err = models.AddOauth2(oa); err != nil { | ||||
log.Error("add oauth2 %v", err) // 501 | log.Error("add oauth2 %v", err) // 501 | ||||
return | return | ||||
} | } | ||||
case models.ErrOauth2NotAssociatedWithUser: | 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: | default: | ||||
log.Error(err.Error()) // FIXME: handle error page | |||||
log.Error("other error: %v", err) | |||||
ctx.Handle(500, err.Error(), nil) | |||||
return | return | ||||
} | } | ||||
ctx.Session.Set("socialId", oa.Id) | 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) | 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) { | m.Group("/user", func(r martini.Router) { | ||||
r.Get("/login", user.SignIn) | r.Get("/login", user.SignIn) | ||||
r.Post("/login", bindIgnErr(auth.LogInForm{}), user.SignInPost) | 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.Get("/sign_up", user.SignUp) | ||||
r.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost) | r.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost) | ||||
r.Get("/reset_password", user.ResetPasswd) | r.Get("/reset_password", user.ResetPasswd) | ||||