From 2ce0c3befef3becd48660e600ef70e8affd5bc7c Mon Sep 17 00:00:00 2001 From: skyblue Date: Sat, 12 Apr 2014 23:19:17 +0800 Subject: [PATCH 1/2] add google oauth2 support --- models/oauth2.go | 4 +- routers/user/social.go | 148 ++++++++++++---------------------- routers/user/social_github.go | 68 ++++++++++++++++ routers/user/social_google.go | 66 +++++++++++++++ web.go | 2 +- 5 files changed, 190 insertions(+), 98 deletions(-) create mode 100644 routers/user/social_github.go create mode 100644 routers/user/social_google.go diff --git a/models/oauth2.go b/models/oauth2.go index c5d58c074..e28b00870 100644 --- a/models/oauth2.go +++ b/models/oauth2.go @@ -26,7 +26,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 +48,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) diff --git a/routers/user/social.go b/routers/user/social.go index 3e5f69bef..cd5958aa0 100644 --- a/routers/user/social.go +++ b/routers/user/social.go @@ -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 BasicUserInfo struct { + Identity string + Name string + Email string +} + type SocialConnector interface { - Identity() string - Name() string - Email() string - TokenString() string -} + Type() int + SetRedirectUrl(string) + UserInfo(*oauth.Token) (*BasicUserInfo, error) -type SocialGithub struct { - data struct { - Id int `json:"id"` - Name string `json:"login"` - Email string `json:"email"` - } - Token *oauth.Token -} - -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) -} - -// 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.RequestURI()) + ctx.Redirect(connect.AuthCodeURL(next)) return } // handle call back - tk, err := transport.Exchange(code) + tk, err := connect.Exchange(code) // transport.Exchange(code) if err != nil { log.Error("oauth2 handle callback error: %v", err) - return // FIXME, need error page 501 - } - next = extractPath(ctx.Query("state")) - log.Debug("success token: %v", tk) - - gh := &SocialGithub{Token: tk} - if err = gh.Update(); err != nil { - // FIXME: handle error page 501 - log.Error("connect with github error: %s", err) + ctx.Handle(500, "exchange code error", nil) return } - var soc SocialConnector = gh - log.Info("login: %s", soc.Name()) - oa, err := models.GetOauth2(soc.Identity()) + next = extractPath(ctx.Query("state")) + log.Trace("success get token") + + ui, err := connect.UserInfo(tk) + 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 + } + 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) } diff --git a/routers/user/social_github.go b/routers/user/social_github.go new file mode 100644 index 000000000..316dc37ad --- /dev/null +++ b/routers/user/social_github.go @@ -0,0 +1,68 @@ +package user + +import ( + "encoding/json" + "net/http" + "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) (*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 +} diff --git a/routers/user/social_google.go b/routers/user/social_google.go new file mode 100644 index 000000000..ecd090cd1 --- /dev/null +++ b/routers/user/social_google.go @@ -0,0 +1,66 @@ +package user + +import ( + "encoding/json" + "net/http" + "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) (*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 +} diff --git a/web.go b/web.go index 13af425b9..de8e72505 100644 --- a/web.go +++ b/web.go @@ -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) From 75d2affcbf9b26c5a989a75a6654d9b6e21415e9 Mon Sep 17 00:00:00 2001 From: skyblue Date: Sun, 13 Apr 2014 01:15:19 +0800 Subject: [PATCH 2/2] add oauth2 qq support --- models/oauth2.go | 1 + routers/user/social.go | 8 ++-- routers/user/social_github.go | 7 ++- routers/user/social_google.go | 7 ++- routers/user/social_qq.go | 83 +++++++++++++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 routers/user/social_qq.go diff --git a/models/oauth2.go b/models/oauth2.go index e28b00870..f8780fe68 100644 --- a/models/oauth2.go +++ b/models/oauth2.go @@ -13,6 +13,7 @@ const ( OT_GITHUB = iota + 1 OT_GOOGLE OT_TWITTER + OT_QQ ) var ( diff --git a/routers/user/social.go b/routers/user/social.go index cd5958aa0..ea47d71b1 100644 --- a/routers/user/social.go +++ b/routers/user/social.go @@ -28,7 +28,7 @@ type BasicUserInfo struct { type SocialConnector interface { Type() int SetRedirectUrl(string) - UserInfo(*oauth.Token) (*BasicUserInfo, error) + UserInfo(*oauth.Token, *url.URL) (*BasicUserInfo, error) AuthCodeURL(string) string Exchange(string) (*oauth.Token, error) @@ -63,13 +63,13 @@ func SocialSignIn(params martini.Params, ctx *middleware.Context) { code := ctx.Query("code") if code == "" { // redirect to social login page - connect.SetRedirectUrl(strings.TrimSuffix(base.AppUrl, "/") + ctx.Req.URL.RequestURI()) + 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 := connect.Exchange(code) // transport.Exchange(code) + tk, err := connect.Exchange(code) // exchange for token if err != nil { log.Error("oauth2 handle callback error: %v", err) ctx.Handle(500, "exchange code error", nil) @@ -78,7 +78,7 @@ func SocialSignIn(params martini.Params, ctx *middleware.Context) { next = extractPath(ctx.Query("state")) log.Trace("success get token") - ui, err := connect.UserInfo(tk) + 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) diff --git a/routers/user/social_github.go b/routers/user/social_github.go index 316dc37ad..e532efd0a 100644 --- a/routers/user/social_github.go +++ b/routers/user/social_github.go @@ -1,8 +1,13 @@ +// 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" @@ -42,7 +47,7 @@ func (s *SocialGithub) SetRedirectUrl(url string) { s.Transport.Config.RedirectURL = url } -func (s *SocialGithub) UserInfo(token *oauth.Token) (*BasicUserInfo, error) { +func (s *SocialGithub) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) { transport := &oauth.Transport{ Token: token, } diff --git a/routers/user/social_google.go b/routers/user/social_google.go index ecd090cd1..b585386f2 100644 --- a/routers/user/social_google.go +++ b/routers/user/social_google.go @@ -1,8 +1,13 @@ +// 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" @@ -40,7 +45,7 @@ func (s *SocialGoogle) SetRedirectUrl(url string) { s.Transport.Config.RedirectURL = url } -func (s *SocialGoogle) UserInfo(token *oauth.Token) (*BasicUserInfo, error) { +func (s *SocialGoogle) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) { transport := &oauth.Transport{Token: token} var data struct { Id string `json:"id"` diff --git a/routers/user/social_qq.go b/routers/user/social_qq.go new file mode 100644 index 000000000..d08892ef8 --- /dev/null +++ b/routers/user/social_qq.go @@ -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 +}