Repository: danilopolani/gocialite Branch: master Commit: e2e2f2eecba8 Files: 19 Total size: 31.2 KB Directory structure: gitextract_p4top8w6/ ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── drivers/ │ ├── amazon.go │ ├── asana.go │ ├── bitbucket.go │ ├── drivers.go │ ├── facebook.go │ ├── foursquare.go │ ├── github.go │ ├── google.go │ ├── linkedin.go │ └── slack.go ├── gocialite.go ├── gocialite_test.go └── structs/ └── user.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store ================================================ FILE: .travis.yml ================================================ language: go go: - 1.9.x - 1.10.x - 1.11.x - 1.12.x - master ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.0.1] - 2019-03-28 ### Fixed - Fix email from Github when is private. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Creating a new driver for Gocialite is a lot simple, thank also to @vibbix. ## Create a file I suggest you to duplicate `drivers/bitbucket.go` since it's a complete code and name it with your provider name, ex: *myprovider.go*. ## Set variables Change [line 12](https://github.com/danilopolani/gocialite/blob/master/drivers/bitbucket.go#L12) with: ```go const myProviderDriverName = "myprovider" ``` It will be used in the `Driver()` function, example: `gocial.Driver("myprovider")`. Now on [line 19](https://github.com/danilopolani/gocialite/blob/master/drivers/bitbucket.go#L19) you have to create the mapping from API to populate the User struct. The relation is `"json_field_name": "StructFieldName"`, so if in our JSON there's a field called "first_name", it will be `"first_name": "FirstName"`. If there's some nested/complex field, please see the next chapter **User callback hook**. Finally, on [lines 26-30](https://github.com/danilopolani/gocialite/blob/master/drivers/bitbucket.go#L26-L30) you have to fill the fields for the endpoint baseurl and the path of the user endpoint. In the case of Bitbucket, the email address is retrievable only from another endpoint, so we put in it also `emailEndpoint`, but usually you will need only `userEndpoint`. If your provider has the user endpoint located to `https://api.myprovider.com/me`, the struct will be this: ```go var MyProviderAPIMap = map[string]string{ "endpoint": "https://api.myprovider.com", "userEndpoint": "/me", } ``` Of course remember to **rename all the variables**. The ones that start with a capital letter are exported, so remember to write the first letter capitalized. ## User callback hook If you have some complex field or you need to call some other endpoint in order to retrieve a field, you can do that in this section. In the case of Bitbucket, we use this hook to populate two fields: *avatar* from a nested array/map and *email* from another endpoint. The `client` variable is an `oAuth` client so it's already set up for oAuth details like `access_token`. ## Testing Use the [example page](https://github.com/danilopolani/gocialite/wiki/Example) as starting point. Set up the credentials of your app in the `providerSecrets` variable, like: ```go providerSecrets := map[string]map[string]string{ ... "myprovider": { "clientID": "xxxxxxxxxxxxxx", "clientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "redirectURL": "http://localhost:9090/auth/myprovider/callback", }, } ``` And add the scopes for your app (or an empty slice) in the `providerScopes` variable: ```go providerScopes := map[string][]string{ ... "myprovider": []string{}, // Or []string{"my_scope", "my_other_scope"} } ``` Now run `go run example.go` (or the name of your file), navigate to http://localhost:9090/auth/myprovider (or the name of your provider) and you will be redirected to the oAuth login. If everything works correctly, when you will be redirected to http://localhost:9090/auth/myprovider/callback, in your terminal you will see the content of the `User` struct populated (line `fmt.Printf("%#v", gocial.User)`). ## PR Now that everything works, you can open a Pull Request, it will be tested and if it works, it will be merged and added to the README. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Danilo Polani Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Gocialite ![Travis CI build](https://api.travis-ci.org/danilopolani/gocialite.svg?branch=master) ![Available Drivers](https://img.shields.io/badge/Drivers-5+-orange.svg) [![GoDoc](https://godoc.org/github.com/danilopolani/gocialite?status.svg)](https://godoc.org/github.com/danilopolani/gocialite) [![GoReport](https://goreportcard.com/badge/github.com/danilopolani/gocialite)](https://goreportcard.com/report/github.com/danilopolani/gocialite) ![GitHub contributors](https://img.shields.io/github/contributors/danilopolani/gocialite.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) ## NOT MAINTAINED This project is no longer maintained, you should over a more robust solution like [Goth](https://github.com/markbates/goth). ---- Gocialite is a Socialite inspired package to manage social oAuth authentication without problems. The idea was born when I discovered that Goth is not so flexible: I was using Revel and it was *impossible* to connect them properly. ## Installation To install it, just run `go get gopkg.in/danilopolani/gocialite.v1` and include it in your app: `import "gopkg.in/danilopolani/gocialite.v1"`. ## Available drivers - Amazon - Asana - Bitbucket - Facebook - Foursquare - Github - Google - LinkedIn - Slack ## Create new driver Please see [Contributing page](https://github.com/danilopolani/gocialite/blob/master/CONTRIBUTING.md) to learn how to create new driver and test it. ## Set scopes **Note**: Gocialite set some default scopes for the user profile, for example for *Facebook* it specify `email` and for *Google* `profile, email`. When you use the following method, you don't have to rewrite them. Use the `Scopes([]string)` method of your `Gocial` instance. Example: ```go gocial.Scopes([]string{"public_repo"}) ``` ## Set driver Use the `Driver(string)` method of your `Gocial` instance. Example: ```go gocial.Driver("facebook") ``` The driver name will be the provider name in lowercase. ## How to use it **Note**: All Gocialite methods are chainable. Declare a "global" variable outside your `main` func: ```go import ( ... ) var gocial = gocialite.NewDispatcher() func main() { ``` Then create a route to use as redirect bridge, for example `/auth/github`. With this route, the user will be redirected to the provider oAuth login. In this case we use Gin Tonic as router. You have to specify the provider with the `Driver()` method. Then, with `Scopes()`, you can set a list of scopes as slice of strings. It's optional. Finally, with `Redirect()` you can obtain the redirect URL. In this method you have to pass three parameters: 1. Client ID 1. Client Secret 1. Redirect URL ```go func main() { router := gin.Default() router.GET("/auth/github", redirectHandler) router.Run("127.0.0.1:9090") } // Redirect to correct oAuth URL func redirectHandler(c *gin.Context) { authURL, err := gocial.New(). Driver("github"). // Set provider Scopes([]string{"public_repo"}). // Set optional scope(s) Redirect( // "xxxxxxxxxxxxxx", // Client ID "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // Client Secret "http://localhost:9090/auth/github/callback", // Redirect URL ) // Check for errors (usually driver not valid) if err != nil { c.Writer.Write([]byte("Error: " + err.Error())) return } // Redirect with authURL c.Redirect(http.StatusFound, authURL) // Redirect with 302 HTTP code } ``` Now create a callback handler route, where we'll receive the content from the provider. In order to validate the oAuth and retrieve the data, you have to invoke the `Handle()` method with two query parameters: `state` and `code`. In your URL, they will look like this: `http://localhost:9090/auth/github/callback?state=xxxxxxxx&code=xxxxxxxx`. The `Handle()` method returns the user info, the token and error if there's one or `nil`. If there are no errors, in the `user` variable you will find the logged in user information and in the `token` one, the token info (it's a [oauth2.Token struct](https://godoc.org/golang.org/x/oauth2#Token)). The data of the user - which is a [gocialite.User struct](https://github.com/danilopolani/gocialite/blob/master/structs/user.go) - are the following: - ID - FirstName - LastName - FullName - Email - Avatar (URL) - Raw (the full JSON returned by the provider) Note that they can be empty. ```go func main() { router := gin.Default() router.GET("/auth/github", redirectHandler) router.GET("/auth/github/callback", callbackHandler) router.Run("127.0.0.1:9090") } // Redirect to correct oAuth URL // Handle callback of provider func callbackHandler(c *gin.Context) { // Retrieve query params for code and state code := c.Query("code") state := c.Query("state") // Handle callback and check for errors user, token, err := gocial.Handle(state, code) if err != nil { c.Writer.Write([]byte("Error: " + err.Error())) return } // Print in terminal user information fmt.Printf("%#v", token) fmt.Printf("%#v", user) // If no errors, show provider name c.Writer.Write([]byte("Hi, " + user.FullName)) } ``` Please take a look to [multi provider example](https://github.com/danilopolani/gocialite/wiki/Multi-provider-example) for a full working code with Gin Tonic and variable provider handler. ## Contributors - [Danilo Polani](https://github.com/danilopolani) - [Joseph Buchma](https://github.com/josephbuchma) - [Mark Beznos](https://github.com/vibbix) - [Davor Kapsa](https://github.com/dvrkps) ================================================ FILE: drivers/amazon.go ================================================ package drivers import ( "net/http" "github.com/danilopolani/gocialite/structs" "golang.org/x/oauth2/amazon" ) const amazonDriverName = "amazon" func init() { registerDriver(amazonDriverName, AmazonDefaultScopes, AmazonUserFn, amazon.Endpoint, AmazonAPIMap, AmazonUserMap) } // AmazonUserMap is the map to create the User struct var AmazonUserMap = map[string]string{ "user_id": "ID", "name": "FullName", "email": "Email", } // AmazonAPIMap is the map for API endpoints var AmazonAPIMap = map[string]string{ "endpoint": "https://api.amazon.com", "userEndpoint": "/user/profile", } // AmazonUserFn is a callback to parse additional fields for User var AmazonUserFn = func(client *http.Client, u *structs.User) {} // AmazonDefaultScopes contains the default scopes var AmazonDefaultScopes = []string{"profile"} ================================================ FILE: drivers/asana.go ================================================ package drivers import ( "net/http" "fmt" "github.com/danilopolani/gocialite/structs" "golang.org/x/oauth2" ) const asanaDriverName = "asana" func init() { registerDriver(asanaDriverName, AsanaDefaultScopes, AsanaUserFn, AsanaEndpoint, AsanaAPIMap, AsanaUserMap) } // DailyMotionEndpoint is the oAuth endpoint var AsanaEndpoint = oauth2.Endpoint{ AuthURL: "https://app.asana.com/-/oauth_authorize", TokenURL: "https://app.asana.com/-/oauth_token", } // AsanaUserMap is the map to create the User struct var AsanaUserMap = map[string]string{} // AsanaAPIMap is the map for API endpoints var AsanaAPIMap = map[string]string{ "endpoint": "https://app.asana.com/api/1.0", "userEndpoint": "/users/me?opt_fields=id,name,email,photo", } // AsanaUserFn is a callback to parse additional fields for User var AsanaUserFn = func(client *http.Client, u *structs.User) { userData := u.Raw["data"].(map[string]interface{}) u.ID = fmt.Sprintf("%.0f", userData["id"].(float64)) u.Email = userData["email"].(string) u.FullName = userData["name"].(string) // Set avatar if (userData["photo"] != nil) { u.Avatar = userData["photo"].(map[string]interface{})["image_1024x1024"].(string) } } // AsanaDefaultScopes contains the default scopes var AsanaDefaultScopes = []string{} ================================================ FILE: drivers/bitbucket.go ================================================ package drivers import ( "io/ioutil" "net/http" "github.com/danilopolani/gocialite/structs" "golang.org/x/oauth2/bitbucket" ) const bitbucketDriverName = "bitbucket" func init() { registerDriver(bitbucketDriverName, BitbucketDefaultScopes, BitbucketUserFn, bitbucket.Endpoint, BitbucketAPIMap, BitbucketUserMap) } // BitbucketUserMap is the map to create the User struct var BitbucketUserMap = map[string]string{ "account_id": "ID", "username": "Username", "display_name": "FullName", } // BitbucketAPIMap is the map for API endpoints var BitbucketAPIMap = map[string]string{ "endpoint": "https://api.bitbucket.org", "userEndpoint": "/2.0/user", "emailEndpoint": "/2.0/user/emails", } // BitbucketUserFn is a callback to parse additional fields for User var BitbucketUserFn = func(client *http.Client, u *structs.User) { // Set avatar u.Avatar = u.Raw["links"].(map[string]interface{})["avatar"].(map[string]interface{})["href"].(string) // Retrieve email req, err := client.Get(BitbucketAPIMap["endpoint"] + BitbucketAPIMap["emailEndpoint"]) if err != nil { return } defer req.Body.Close() res, _ := ioutil.ReadAll(req.Body) data, err := jsonDecode(res) if err != nil { return } u.Email = data["values"].([]interface{})[0].(map[string]interface{})["email"].(string) } // BitbucketDefaultScopes contains the default scopes var BitbucketDefaultScopes = []string{"account", "email"} ================================================ FILE: drivers/drivers.go ================================================ package drivers import ( "encoding/json" "net/http" "github.com/danilopolani/gocialite/structs" "golang.org/x/oauth2" ) var ( initAPIMap = map[string]map[string]string{} initUserMap = map[string]map[string]string{} initEndpointMap = map[string]oauth2.Endpoint{} initCallbackMap = map[string]func(client *http.Client, u *structs.User){} initDefaultScopesMap = map[string][]string{} ) func registerDriver(driver string, defaultscopes []string, callback func(client *http.Client, u *structs.User), endpoint oauth2.Endpoint, apimap, usermap map[string]string) { initAPIMap[driver] = apimap initUserMap[driver] = usermap initEndpointMap[driver] = endpoint initCallbackMap[driver] = callback initDefaultScopesMap[driver] = defaultscopes } // InitializeDrivers adds all the drivers to the register func func InitializeDrivers(register func(driver string, defaultscopes []string, callback func(client *http.Client, u *structs.User), endpoint oauth2.Endpoint, apimap, usermap map[string]string)) { for k := range initAPIMap { register(k, initDefaultScopesMap[k], initCallbackMap[k], initEndpointMap[k], initAPIMap[k], initUserMap[k]) } } // Decode a json or return an error func jsonDecode(js []byte) (map[string]interface{}, error) { var decoded map[string]interface{} if err := json.Unmarshal(js, &decoded); err != nil { return nil, err } return decoded, nil } ================================================ FILE: drivers/facebook.go ================================================ package drivers import ( "net/http" "github.com/danilopolani/gocialite/structs" "golang.org/x/oauth2/facebook" ) const facebookDriverName = "facebook" func init() { registerDriver(facebookDriverName, FacebookDefaultScopes, FacebookUserFn, facebook.Endpoint, FacebookAPIMap, FacebookUserMap) } // FacebookUserMap is the map to create the User struct var FacebookUserMap = map[string]string{ "id": "ID", "email": "Email", "name": "FullName", "first_name": "FirstName", "last_name": "LastName", } // FacebookAPIMap is the map for API endpoints var FacebookAPIMap = map[string]string{ "endpoint": "https://graph.facebook.com", "userEndpoint": "/me?fields=id,name,first_name,last_name,email", } // FacebookUserFn is a callback to parse additional fields for User var FacebookUserFn = func(client *http.Client, u *structs.User) { u.Avatar = FacebookAPIMap["endpoint"] + "/v2.8/" + u.ID + "/picture?width=800" } // FacebookDefaultScopes contains the default scopes var FacebookDefaultScopes = []string{"email"} ================================================ FILE: drivers/foursquare.go ================================================ package drivers import ( "net/http" "github.com/danilopolani/gocialite/structs" "golang.org/x/oauth2/foursquare" ) const foursquareDriverName = "foursquare" func init() { registerDriver(foursquareDriverName, FoursquareDefaultScopes, FoursquareUserFn, foursquare.Endpoint, FoursquareAPIMap, FoursquareUserMap) } // FoursquareUserMap is the map to create the User struct var FoursquareUserMap = map[string]string{} // FoursquareAPIMap is the map for API endpoints var FoursquareAPIMap = map[string]string{ "endpoint": "https://api.foursquare.com", "userEndpoint": "/v2/users/self?oauth_token=%ACCESS_TOKEN&v=20171220", } // FoursquareUserFn is a callback to parse additional fields for User var FoursquareUserFn = func(client *http.Client, u *structs.User) { user := u.Raw["response"].(map[string]interface{})["user"].(map[string]interface{}) u.ID = user["id"].(string) u.FirstName = user["firstName"].(string) u.LastName = user["lastName"].(string) u.FullName = u.FirstName + " " + u.LastName if email, ok := user["contact"].(map[string]interface{})["email"]; ok { u.Email = email.(string) } if avatarPrefix, ok := user["photo"].(map[string]interface{})["prefix"]; ok { if avatarSuffix, ok2 := user["photo"].(map[string]interface{})["suffix"]; ok2 { u.Avatar = avatarPrefix.(string) + "original" + avatarSuffix.(string) } } } // FoursquareDefaultScopes contains the default scopes var FoursquareDefaultScopes = []string{} ================================================ FILE: drivers/github.go ================================================ package drivers import ( "encoding/json" "net/http" "github.com/danilopolani/gocialite/structs" "golang.org/x/oauth2/github" ) const githubDriverName = "github" func init() { registerDriver(githubDriverName, GithubDefaultScopes, GithubUserFn, github.Endpoint, GithubAPIMap, GithubUserMap) } // GithubUserMap is the map to create the User struct var GithubUserMap = map[string]string{ "id": "ID", "email": "Email", "login": "Username", "avatar_url": "Avatar", "name": "FullName", } // GithubAPIMap is the map for API endpoints var GithubAPIMap = map[string]string{ "endpoint": "https://api.github.com", "userEndpoint": "/user", "emailEndpoint": "/user/emails", } // GithubUserFn is a callback to parse additional fields for User var GithubUserFn = func(client *http.Client, u *structs.User) { // Used to parse the email from response type additionalEmail struct { Email string `json:"email"` } var email []additionalEmail // Email can be nil because of the "keep my email private" setting if u.Email == "" { // Retrieve email req, err := client.Get(GithubAPIMap["endpoint"] + GithubAPIMap["emailEndpoint"]) if err != nil { return } defer req.Body.Close() err = json.NewDecoder(req.Body).Decode(&email) if err != nil { return } u.Email = email[0].Email } } // GithubDefaultScopes contains the default scopes var GithubDefaultScopes = []string{"user:email"} ================================================ FILE: drivers/google.go ================================================ package drivers import ( "net/http" "github.com/danilopolani/gocialite/structs" "golang.org/x/oauth2/google" ) const googleDriverName = "google" func init() { registerDriver(googleDriverName, GoogleDefaultScopes, GoogleUserFn, google.Endpoint, GoogleAPIMap, GoogleUserMap) } // GoogleUserMap is the map to create the User struct var GoogleUserMap = map[string]string{ "id": "ID", "email": "Email", "name": "FullName", "given_name": "FirstName", "family_name": "LastName", "picture": "Avatar", } // GoogleAPIMap is the map for API endpoints var GoogleAPIMap = map[string]string{ "endpoint": "https://www.googleapis.com", "userEndpoint": "/oauth2/v2/userinfo", } // GoogleUserFn is a callback to parse additional fields for User var GoogleUserFn = func(client *http.Client, u *structs.User) {} // GoogleDefaultScopes contains the default scopes var GoogleDefaultScopes = []string{"profile", "email"} ================================================ FILE: drivers/linkedin.go ================================================ package drivers import ( "net/http" "github.com/danilopolani/gocialite/structs" "golang.org/x/oauth2/linkedin" ) const ( linkedinDriverName = "linkedin" ) func init() { registerDriver(linkedinDriverName, LinkedInDefaultScopes, LinkedInUserFn, linkedin.Endpoint, LinkedInAPIMap, LinkedInUserMap) } // LinkedInUserMap is the map to create the User struct var LinkedInUserMap = map[string]string{ "id": "ID", "vanityName": "Username", "firstName": "FirstName", "lastName": "LastName", "formattedName": "FullName", "emailAddress": "Email", "pictureUrl": "Avatar", } // LinkedInAPIMap is the map for API endpoints var LinkedInAPIMap = map[string]string{ "endpoint": "https://api.linkedin.com", "userEndpoint": "/v1/people/~:(id,first-name,last-name,formatted-name,email-address,picture-url,maiden-name,headline,location,industry,current-share,num-connections,summary,specialties,positions,public-profile-url)?format=json", } // LinkedInUserFn is a callback to parse additional fields for User var LinkedInUserFn = func(client *http.Client, u *structs.User) {} // LinkedInDefaultScopes contains the default scopes var LinkedInDefaultScopes = []string{} ================================================ FILE: drivers/slack.go ================================================ package drivers import ( "io/ioutil" "net/http" "github.com/danilopolani/gocialite/structs" "golang.org/x/oauth2/slack" ) const slackDriverName = "slack" func init() { registerDriver(slackDriverName, SlackDefaultScopes, SlackUserFn, slack.Endpoint, SlackAPIMap, SlackUserMap) } // SlackUserMap is the map to create the User struct var SlackUserMap = map[string]string{ "real_name": "FullName", "first_name": "FirstName", "last_name": "LastName", "email": "Email", "image_original": "Avatar", } // SlackAPIMap is the map for API endpoints var SlackAPIMap = map[string]string{ "endpoint": "https://slack.com/api", "userEndpoint": "/users.profile.get", "authEndpoint": "/auth.test", } // SlackUserFn is a callback to parse additional fields for User var SlackUserFn = func(client *http.Client, u *structs.User) { // Get user ID req, err := client.Get(SlackAPIMap["endpoint"] + SlackAPIMap["authEndpoint"]) if err != nil { return } defer req.Body.Close() res, _ := ioutil.ReadAll(req.Body) data, err := jsonDecode(res) if err != nil { return } u.ID = data["user_id"].(string) // Fetch other user information userInfo := u.Raw["profile"].(map[string]interface{}) u.Username = userInfo["display_name"].(string) u.FullName = userInfo["real_name"].(string) u.FirstName = userInfo["first_name"].(string) u.LastName = userInfo["last_name"].(string) u.Email = userInfo["email"].(string) u.Avatar = userInfo["image_original"].(string) } // SlackDefaultScopes contains the default scopes var SlackDefaultScopes = []string{"users.profile:read"} ================================================ FILE: gocialite.go ================================================ package gocialite import ( "crypto/rand" "encoding/base64" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "strings" "sync" "github.com/danilopolani/gocialite/drivers" "github.com/danilopolani/gocialite/structs" "golang.org/x/oauth2" "gopkg.in/oleiade/reflections.v1" ) // Dispatcher allows to safely issue concurrent Gocials type Dispatcher struct { mu sync.RWMutex g map[string]*Gocial } // NewDispatcher creates new Dispatcher func NewDispatcher() *Dispatcher { return &Dispatcher{g: make(map[string]*Gocial)} } // New Gocial instance func (d *Dispatcher) New() *Gocial { d.mu.Lock() defer d.mu.Unlock() state := randToken() g := &Gocial{state: state} d.g[state] = g return g } // Handle callback. Can be called only once for given state. func (d *Dispatcher) Handle(state, code string) (*structs.User, *oauth2.Token, error) { d.mu.RLock() g, ok := d.g[state] d.mu.RUnlock() if !ok { return nil, nil, fmt.Errorf("invalid CSRF token: %s", state) } err := g.Handle(state, code) d.mu.Lock() delete(d.g, state) d.mu.Unlock() return &g.User, g.Token, err } // Gocial is the main struct of the package type Gocial struct { driver, state string scopes []string conf *oauth2.Config User structs.User Token *oauth2.Token } func init() { drivers.InitializeDrivers(RegisterNewDriver) } var ( // Set the basic information such as the endpoint and the scopes URIs apiMap = map[string]map[string]string{} // Mapping to create a valid "user" struct from providers userMap = map[string]map[string]string{} // Map correct endpoints endpointMap = map[string]oauth2.Endpoint{} // Map custom callbacks callbackMap = map[string]func(client *http.Client, u *structs.User){} // Default scopes for each driver defaultScopesMap = map[string][]string{} ) //RegisterNewDriver adds a new driver to the existing set func RegisterNewDriver(driver string, defaultscopes []string, callback func(client *http.Client, u *structs.User), endpoint oauth2.Endpoint, apimap, usermap map[string]string) { apiMap[driver] = apimap userMap[driver] = usermap endpointMap[driver] = endpoint callbackMap[driver] = callback defaultScopesMap[driver] = defaultscopes } // Driver is needed to choose the correct social func (g *Gocial) Driver(driver string) *Gocial { g.driver = driver g.scopes = defaultScopesMap[driver] // BUG: sequential usage of single Gocial instance will have same CSRF token. This is serious security issue. // NOTE: Dispatcher eliminates this bug. if g.state == "" { g.state = randToken() } return g } // Scopes is used to set the oAuth scopes, for example "user", "calendar" func (g *Gocial) Scopes(scopes []string) *Gocial { g.scopes = append(g.scopes, scopes...) return g } // Redirect returns an URL for the selected social oAuth login func (g *Gocial) Redirect(clientID, clientSecret, redirectURL string) (string, error) { // Check if driver is valid if !inSlice(g.driver, complexKeys(apiMap)) { return "", fmt.Errorf("Driver not valid: %s", g.driver) } // Check if valid redirectURL _, err := url.ParseRequestURI(redirectURL) if err != nil { return "", fmt.Errorf("Redirect URL <%s> not valid: %s", redirectURL, err.Error()) } if !strings.HasPrefix(redirectURL, "http://") && !strings.HasPrefix(redirectURL, "https://") { return "", fmt.Errorf("Redirect URL <%s> not valid: protocol not valid", redirectURL) } g.conf = &oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, RedirectURL: redirectURL, Scopes: g.scopes, Endpoint: endpointMap[g.driver], } return g.conf.AuthCodeURL(g.state), nil } // Handle callback from provider func (g *Gocial) Handle(state, code string) error { // Handle the exchange code to initiate a transport. if g.state != state { return fmt.Errorf("Invalid state: %s", state) } // Check if driver is valid if !inSlice(g.driver, complexKeys(apiMap)) { return fmt.Errorf("Driver not valid: %s", g.driver) } token, err := g.conf.Exchange(oauth2.NoContext, code) if err != nil { return fmt.Errorf("oAuth exchanged failed: %s", err.Error()) } client := g.conf.Client(oauth2.NoContext, token) // Set gocial token g.Token = token // Retrieve all from scopes driverAPIMap := apiMap[g.driver] driverUserMap := userMap[g.driver] userEndpoint := strings.Replace(driverAPIMap["userEndpoint"], "%ACCESS_TOKEN", token.AccessToken, -1) // Get user info req, err := client.Get(driverAPIMap["endpoint"] + userEndpoint) if err != nil { return err } defer req.Body.Close() res, _ := ioutil.ReadAll(req.Body) data, err := jsonDecode(res) if err != nil { return fmt.Errorf("Error decoding JSON: %s", err.Error()) } // Scan all fields and dispatch through the mapping mapKeys := keys(driverUserMap) gUser := structs.User{} for k, f := range data { if !inSlice(k, mapKeys) { // Skip if not in the mapping continue } // Assign the value // Dirty way, but we need to convert also int/float to string _ = reflections.SetField(&gUser, driverUserMap[k], fmt.Sprint(f)) } // Set the "raw" user interface gUser.Raw = data // Custom callback callbackMap[g.driver](client, &gUser) // Update the struct g.User = gUser return nil } // Generate a random token func randToken() string { b := make([]byte, 32) rand.Read(b) return base64.StdEncoding.EncodeToString(b) } // Check if a value is in a string slice func inSlice(v string, s []string) bool { for _, scope := range s { if scope == v { return true } } return false } // Decode a json or return an error func jsonDecode(js []byte) (map[string]interface{}, error) { var decoded map[string]interface{} decoder := json.NewDecoder(strings.NewReader(string(js))) decoder.UseNumber() if err := decoder.Decode(&decoded); err != nil { return nil, err } return decoded, nil } // Return the keys of a map func keys(m map[string]string) []string { var keys []string for k := range m { keys = append(keys, k) } return keys } func complexKeys(m map[string]map[string]string) []string { var keys []string for k := range m { keys = append(keys, k) } return keys } ================================================ FILE: gocialite_test.go ================================================ package gocialite import ( "testing" "github.com/stretchr/testify/assert" ) var gocialTest Gocial func TestScopes(t *testing.T) { gocialTest.Scopes([]string{"email"}) assert.Equal(t, gocialTest.scopes, []string{"email"}) assert.NotEqual(t, gocialTest.scopes, []string{}) gocialTest. Driver("google"). Scopes([]string{"calendar.readonly"}) assert.Equal(t, gocialTest.scopes, []string{"profile", "email", "calendar.readonly"}) assert.NotEqual(t, gocialTest.scopes, []string{"profile", "email"}) assert.NotEqual(t, gocialTest.scopes, []string{}) } func TestConf(t *testing.T) { assert := assert.New(t) gocialTest. Driver("github"). Redirect( "foo", "bar", "http://example.com/auth/callback", ) assert.Equal(gocialTest.conf.ClientID, "foo") assert.NotEqual(gocialTest.conf.ClientID, "") assert.NotNil(gocialTest.conf.ClientID) assert.Equal(gocialTest.conf.ClientSecret, "bar") assert.NotEqual(gocialTest.conf.ClientSecret, "") assert.NotNil(gocialTest.conf.ClientSecret) assert.Equal(gocialTest.conf.RedirectURL, "http://example.com/auth/callback") assert.NotEqual(gocialTest.conf.RedirectURL, "") assert.NotNil(gocialTest.conf.RedirectURL) } func TestDriver(t *testing.T) { var err error _, err = gocialTest.Driver("unknown"). Redirect( "xxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxx", "http://example.com/auth/callback", ) assert.NotNil(t, err) _, err = gocialTest.Driver("github"). Redirect( "xxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxx", "http://example.com/auth/callback", ) assert.Nil(t, err) } func TestRedirectURL(t *testing.T) { var err error _, err = gocialTest.Driver("github"). Redirect( "xxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxx", "/auth/callback", ) assert.NotNil(t, err) _, err = gocialTest.Driver("github"). Redirect( "xxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxx", "http://example.com/auth/callback", ) assert.Nil(t, err) } func TestState(t *testing.T) { var err error err = gocialTest.Driver("github"). Handle("fakeState", "foo") assert.NotNil(t, err) } func TestExchange(t *testing.T) { var err error // Generate a state gocialTest. Driver("github"). Redirect( "xxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxx", "http://example.com/auth/callback", ) err = gocialTest.Handle(gocialTest.state, "foo") assert.NotNil(t, err) } ================================================ FILE: structs/user.go ================================================ package structs // User struct type User struct { ID string `json:"id"` Username string `json:"username"` FirstName string `json:"first_name"` LastName string `json:"last_name"` FullName string `json:"full_name"` Email string `json:"email"` Avatar string `json:"avatar"` Raw map[string]interface{} `json:"raw"` // Raw data }