diff --git a/kit/oauth/github.go b/kit/oauth/github.go new file mode 100644 index 0000000..8872cc0 --- /dev/null +++ b/kit/oauth/github.go @@ -0,0 +1,184 @@ +package oauth + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "strconv" + "strings" + "time" + + "golang.org/x/oauth2" +) + +type GithubAuth struct { + Client *http.Client + Config *oauth2.Config +} + +func NewGithubAuth(clientId string, clientSecret string) *GithubAuth { + auth := &GithubAuth{ + Config: &oauth2.Config{ + Scopes: []string{"user:email", "read:user"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://github.com/login/oauth/authorize", + TokenURL: "https://github.com/login/oauth/access_token", + }, + ClientID: clientId, + ClientSecret: clientSecret, + }, + Client: defaultHttpClient, + } + + return auth +} + +type GithubToken struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` + Error string `json:"error"` +} + +func (auth *GithubAuth) GetToken(code string) (*oauth2.Token, error) { + if len(auth.Config.ClientID) == 0 || len(auth.Config.ClientSecret) == 0 { + return nil, fmt.Errorf("Github OAuth client id or secret is empty, please set in config first") + } + + params := &struct { + Code string `json:"code"` + ClientId string `json:"client_id"` + ClientSecret string `json:"client_secret"` + }{code, auth.Config.ClientID, auth.Config.ClientSecret} + data, err := auth.postWithBody(params, auth.Config.Endpoint.TokenURL) + if err != nil { + return nil, err + } + pToken := &GithubToken{} + if err = json.Unmarshal(data, pToken); err != nil { + return nil, err + } + if pToken.Error != "" { + return nil, fmt.Errorf("err: %s", pToken.Error) + } + + token := &oauth2.Token{ + AccessToken: pToken.AccessToken, + TokenType: "Bearer", + } + + return token, nil + +} + +// https://github.com/login/oauth/authorize?client_id=85db232fde2c9320ece7&redirect_uri=http://localhost:8080/api/auth/github&scope=user&state=weave_state +type GitHubUserInfo struct { + Login string `json:"login"` + ID int `json:"id"` + NodeId string `json:"node_id"` + AvatarUrl string `json:"avatar_url"` + GravatarId string `json:"gravatar_id"` + Url string `json:"url"` + HtmlUrl string `json:"html_url"` + FollowersUrl string `json:"followers_url"` + FollowingUrl string `json:"following_url"` + GistsUrl string `json:"gists_url"` + StarredUrl string `json:"starred_url"` + SubscriptionsUrl string `json:"subscriptions_url"` + OrganizationsUrl string `json:"organizations_url"` + ReposUrl string `json:"repos_url"` + EventsUrl string `json:"events_url"` + ReceivedEventsUrl string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + Name string `json:"name"` + Company string `json:"company"` + Blog string `json:"blog"` + Location string `json:"location"` + Email string `json:"email"` + Hireable bool `json:"hireable"` + Bio string `json:"bio"` + TwitterUsername interface{} `json:"twitter_username"` + PublicRepos int `json:"public_repos"` + PublicGists int `json:"public_gists"` + Followers int `json:"followers"` + Following int `json:"following"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PrivateGists int `json:"private_gists"` + TotalPrivateRepos int `json:"total_private_repos"` + OwnedPrivateRepos int `json:"owned_private_repos"` + DiskUsage int `json:"disk_usage"` + Collaborators int `json:"collaborators"` + TwoFactorAuthentication bool `json:"two_factor_authentication"` + Plan struct { + Name string `json:"name"` + Space int `json:"space"` + Collaborators int `json:"collaborators"` + PrivateRepos int `json:"private_repos"` + } `json:"plan"` +} + +func (auth *GithubAuth) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + req, err := http.NewRequest("GET", "https://api.github.com/user", nil) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "token "+token.AccessToken) + resp, err := auth.Client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var githubUserInfo GitHubUserInfo + err = json.Unmarshal(body, &githubUserInfo) + if err != nil { + return nil, err + } + + user := &UserInfo{ + ID: strconv.Itoa(githubUserInfo.ID), + Url: githubUserInfo.Url, + AuthType: GithubAuthType, + Username: githubUserInfo.Login, + DisplayName: githubUserInfo.Name, + Email: githubUserInfo.Email, + AvatarUrl: githubUserInfo.AvatarUrl, + } + return user, nil +} + +func (auth *GithubAuth) postWithBody(body interface{}, url string) ([]byte, error) { + bs, err := json.Marshal(body) + if err != nil { + return nil, err + } + r := strings.NewReader(string(bs)) + req, _ := http.NewRequest("POST", url, r) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + resp, err := auth.Client.Do(req) + if err != nil { + return nil, err + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) + + return data, nil +} diff --git a/kit/oauth/provider.go b/kit/oauth/provider.go new file mode 100644 index 0000000..81eae73 --- /dev/null +++ b/kit/oauth/provider.go @@ -0,0 +1,97 @@ +package oauth + +import ( + "fmt" + "net" + "net/http" + "pandax/kit/model" + "time" + + "golang.org/x/oauth2" +) + +const ( + GithubAuthType = "github" + WeChatAuthType = "wechat" + EmptyAuthType = "nil" +) + +var ( + defaultHttpClient = &http.Client{ + Transport: &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 5 * time.Second, + }).Dial, + TLSHandshakeTimeout: 5 * time.Second, + }, + Timeout: 10 * time.Second, + } +) + +func IsEmptyAuthType(authType string) bool { + return authType == "" || authType == EmptyAuthType +} + +type OAuthConfig struct { + AuthType string `yaml:"authType"` + ClientId string `yaml:"clientId"` + ClientSecret string `yaml:"clientSecret"` +} + +type UserInfo struct { + ID string + Url string + AuthType string + Username string + DisplayName string + Email string + AvatarUrl string +} + +func (ui *UserInfo) User() *model.OauthAccount { + return &model.OauthAccount{ + Name: ui.Username, + Email: ui.Email, + Avatar: ui.AvatarUrl, + AuthInfos: []model.AuthInfo{ + { + AuthType: ui.AuthType, + AuthId: ui.ID, + Url: ui.Url, + }, + }, + } +} + +type OAuthManager struct { + conf map[string]OAuthConfig +} + +func NewOAuthManager(conf map[string]OAuthConfig) *OAuthManager { + return &OAuthManager{ + conf: conf, + } +} + +func (m *OAuthManager) GetAuthProvider(authType string) (AuthProvider, error) { + var provider AuthProvider + conf, ok := m.conf[authType] + if !ok { + return nil, fmt.Errorf("auth type %s not found in config", authType) + } + switch authType { + case GithubAuthType: + provider = NewGithubAuth(conf.ClientId, conf.ClientSecret) + case WeChatAuthType: + provider = NewWeChatAuth(conf.ClientId, conf.ClientSecret) + default: + return nil, fmt.Errorf("unknown auth type: %s", authType) + } + + return provider, nil +} + +type AuthProvider interface { + GetToken(code string) (*oauth2.Token, error) + GetUserInfo(token *oauth2.Token) (*UserInfo, error) +} diff --git a/kit/oauth/wechat.go b/kit/oauth/wechat.go new file mode 100644 index 0000000..2fad989 --- /dev/null +++ b/kit/oauth/wechat.go @@ -0,0 +1,166 @@ +package oauth + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "golang.org/x/oauth2" +) + +type WeChatAuth struct { + Client *http.Client + Config *oauth2.Config +} + +func NewWeChatAuth(clientId string, clientSecret string) *WeChatAuth { + auth := &WeChatAuth{ + Config: &oauth2.Config{ + Scopes: []string{"snsapi_login"}, + Endpoint: oauth2.Endpoint{ + TokenURL: "https://graph.qq.com/oauth2.0/token", + }, + ClientID: clientId, + ClientSecret: clientSecret, + }, + Client: defaultHttpClient, + } + + return auth +} + +type WechatAccessToken struct { + AccessToken string `json:"access_token"` //Interface call credentials + ExpiresIn int64 `json:"expires_in"` //access_token interface call credential timeout time, unit (seconds) + RefreshToken string `json:"refresh_token"` //User refresh access_token + Openid string `json:"openid"` //Unique ID of authorized user + Scope string `json:"scope"` //The scope of user authorization, separated by commas. (,) + Unionid string `json:"unionid"` //This field will appear if and only if the website application has been authorized by the user's UserInfo. +} + +// GetToken use code get access_token (*operation of getting code ought to be done in front) +// get more detail via: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html +func (auth *WeChatAuth) GetToken(code string) (*oauth2.Token, error) { + params := url.Values{} + params.Add("grant_type", "authorization_code") + params.Add("appid", auth.Config.ClientID) + params.Add("secret", auth.Config.ClientSecret) + params.Add("code", code) + + accessTokenUrl := fmt.Sprintf("https://api.weixin.qq.com/sns/oauth2/access_token?%s", params.Encode()) + tokenResponse, err := auth.Client.Get(accessTokenUrl) + if err != nil { + return nil, err + } + + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(tokenResponse.Body) + + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(tokenResponse.Body) + if err != nil { + return nil, err + } + + // {"errcode":40163,"errmsg":"code been used, rid: 6206378a-793424c0-2e4091cc"} + if strings.Contains(buf.String(), "errcode") { + return nil, fmt.Errorf(buf.String()) + } + + var wechatAccessToken WechatAccessToken + if err = json.Unmarshal(buf.Bytes(), &wechatAccessToken); err != nil { + return nil, err + } + + token := oauth2.Token{ + AccessToken: wechatAccessToken.AccessToken, + TokenType: "WeChatAccessToken", + RefreshToken: wechatAccessToken.RefreshToken, + Expiry: time.Time{}, + } + + raw := make(map[string]string) + raw["Openid"] = wechatAccessToken.Openid + token.WithExtra(raw) + + return &token, nil +} + +//{ +// "openid": "of_Hl5zVpyj0vwzIlAyIlnXe1234", +// "nickname": "飞翔的企鹅", +// "sex": 1, +// "language": "zh_CN", +// "city": "Shanghai", +// "province": "Shanghai", +// "country": "CN", +// "headimgurl": "https:\/\/thirdwx.qlogo.cn\/mmopen\/vi_32\/Q0j4TwGTfTK6xc7vGca4KtibJib5dslRianc9VHt9k2N7fewYOl8fak7grRM7nS5V6HcvkkIkGThWUXPjDbXkQFYA\/132", +// "privilege": [], +// "unionid": "oxW9O1VAL8x-zfWP2hrqW9c81234" +//} + +type WechatUserInfo struct { + Openid string `json:"openid"` // The ID of an ordinary user, which is unique to the current developer account + Nickname string `json:"nickname"` // Ordinary user nickname + Sex int `json:"sex"` // Ordinary user gender, 1 is male, 2 is female + Language string `json:"language"` + City string `json:"city"` // City filled in by general user's personal data + Province string `json:"province"` // Province filled in by ordinary user's personal information + Country string `json:"country"` // Country, such as China is CN + Headimgurl string `json:"headimgurl"` // User avatar, the last value represents the size of the square avatar (there are optional values of 0, 46, 64, 96, 132, 0 represents a 640*640 square avatar), this item is empty when the user does not have a avatar + Privilege []string `json:"privilege"` // User Privilege information, json array, such as Wechat Woka user (chinaunicom) + Unionid string `json:"unionid"` // Unified user identification. For an application under a WeChat open platform account, the unionid of the same user is unique. +} + +// GetUserInfo use WechatAccessToken gotten before return WechatUserInfo +// get more detail via: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Authorized_Interface_Calling_UnionID.html +func (auth *WeChatAuth) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + var wechatUserInfo WechatUserInfo + accessToken := token.AccessToken + openid := token.Extra("Openid") + + userInfoUrl := fmt.Sprintf("https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s", accessToken, openid) + resp, err := auth.Client.Get(userInfoUrl) + if err != nil { + return nil, err + } + + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) + + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(resp.Body) + if err != nil { + return nil, err + } + if err = json.Unmarshal(buf.Bytes(), &wechatUserInfo); err != nil { + return nil, err + } + + id := wechatUserInfo.Unionid + if id == "" { + id = wechatUserInfo.Openid + } + + userInfo := UserInfo{ + ID: id, + AuthType: WeChatAuthType, + Username: wechatUserInfo.Nickname, + DisplayName: wechatUserInfo.Nickname, + AvatarUrl: wechatUserInfo.Headimgurl, + } + return &userInfo, nil +}