| @@ -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) | |||