This blog post gives code samples of OAuth authentication with Go (Golang). We will cover the authentication using Facebook, LinkedIn, Github, Twitter and Google Plus.
OAuth authentication is not new and it is wildly used today. In this blog post I try not to reinvent the wheel and give you pointer to many articles that explain each authentication process. You can follow up each link to find more explanations on each one.
In those samples, we try to have few external dependencies beside the one from the standard libraries. All the source code is available on my github.
Those code examples are not fully secure because they do not manage the state parameter required to implement OAuth 2 RFC 6749 10.12 CSRF Protection. Indeed, you need to setup a (short-lived) state cookie and adds tons of code to handle this process. Check out the library of Dalton Hubble here. I think he did a great job implementing those login processes.
This example is inspired from this blog post. This example is a little more concise.
Before running this code, make sure we have our Facebook Developer setting set up correctly. Go on https://developers.facebook.com and add a new App. We make sure:
App Domains=localhost Site URL= http://localhost:8080/
Now that we have an App ID and an App Secret, we setup environment variables like such:
export ENV_FB_CLIENT_ID="111111111111111" export ENV_FB_CLIENT_SECRET="11111111111111111111111111111111" export ENV_FB_REDIRECT_URL="http://localhost:8080/FBLogin"
The code bellow is pretty straightforward.
Try running it using: go run fb_login.go
package main import ( "encoding/json" "fmt" "golang.org/x/oauth2" "golang.org/x/oauth2/facebook" "io/ioutil" "net/http" "os" ) var ( clientID = os.Getenv("ENV_FB_CLIENT_ID") clientSecret = os.Getenv("ENV_FB_CLIENT_SECRET") redirectURL = os.Getenv("ENV_FB_REDIRECT_URL") ) var fbConfig = &oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, RedirectURL: redirectURL, Scopes: []string{"email", "user_birthday", "user_location", "user_about_me"}, Endpoint: facebook.Endpoint, } type user struct { Id string `json:"id"` Email string `json:"email"` Birthday string `json:"birthday"` Username string `json:"name"` } func Home(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") url := fbConfig.AuthCodeURL("") w.Write([]byte("<html><title>Golang Login Facebook Example</title> <body> <a href='" + url + "'><button>Login with Facebook!</button> </a> </body></html>")) } func FBLogin(w http.ResponseWriter, r *http.Request) { var userData user w.Header().Set("Content-Type", "text/html; charset=utf-8") tok, err := fbConfig.Exchange(oauth2.NoContext, r.FormValue("code")) // handle err. You need to change this into something more robust // such as redirect back to home page with error message if err != nil { w.Write([]byte(err.Error())) } response, err := http.Get("https://graph.facebook.com/me?access_token=" + tok.AccessToken) if err != nil { w.Write([]byte(err.Error())) } body, err := ioutil.ReadAll(response.Body) fmt.Println(string(body)) err = json.Unmarshal(body, &userData) // here! fmt.Println(userData) if err != nil { w.Write([]byte(err.Error())) } id := userData.Id bday := userData.Birthday fbusername := userData.Username email := userData.Email w.Write([]byte(fmt.Sprintf("Username %s ID is %s and birthday is %s and email is %s<br>", fbusername, id, bday, email))) img := "https://graph.facebook.com/" + id + "/picture?width=180&height=180" w.Write([]byte("Photo is located at " + img + "<br>")) // see https://www.socketloop.com/tutorials/golang-download-file-example on how to save FB file to disk w.Write([]byte("<img src='" + img + "'>")) } func main() { mux := http.NewServeMux() mux.HandleFunc("/", Home) mux.HandleFunc("/FBLogin", FBLogin) http.ListenAndServe(":8080", mux) }
The LinkedIn authentication is pretty mush the same that the facebook one.
Do not forget to register your application to https://developer.linkedin.com and setup your environment variables.
package main import ( "encoding/json" "fmt" "golang.org/x/oauth2" "golang.org/x/oauth2/linkedin" "io/ioutil" "net/http" "os" ) var ( clientID = os.Getenv("ENV_LINKEDIN_CLIENT_ID") clientSecret = os.Getenv("ENV_LINKEDIN_CLIENT_SECRET") redirectURL = os.Getenv("ENV_LINKEDIN_REDIRECT_URL") ) var lnConfig = &oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, RedirectURL: redirectURL, Scopes: []string{"r_basicprofile"}, Endpoint: linkedin.Endpoint, } type user struct { Id string `json:"id"` FirstName string `json:"firstName"` LastName string `json:"lastName"` Headline string `json:"headline"` } func Home(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") url := lnConfig.AuthCodeURL("") w.Write([]byte("<html><title>Golang Login Linkedin Example</title> <body> <a href='" + url + "&state=DCEeFWf45A53sdfiif424'><button>Login with Linkedin!</button> </a> </body></html>")) } func LinkedinLogin(w http.ResponseWriter, r *http.Request) { var userData user w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Println(r) tok, err := lnConfig.Exchange(oauth2.NoContext, r.FormValue("code")) // handle err. You need to change this into something more robust // such as redirect back to home page with error message if err != nil { w.Write([]byte(err.Error())) } fmt.Println(tok) response, err := http.Get("https://api.linkedin.com/v1/people/~?format=json&oauth2_access_token=" + tok.AccessToken) if err != nil { w.Write([]byte(err.Error())) } body, err := ioutil.ReadAll(response.Body) fmt.Println(string(body)) err = json.Unmarshal(body, &userData) // here! fmt.Println(userData) if err != nil { w.Write([]byte(err.Error())) } id := userData.Id firstname := userData.FirstName lastname := userData.LastName headline := userData.Headline w.Write([]byte(fmt.Sprintf("FirstName %s, LastName %s, ID is %s and headline is %s<br>", firstname, lastname, id, headline))) } func main() { mux := http.NewServeMux() mux.HandleFunc("/", Home) mux.HandleFunc("/LinkedinLogin", LinkedinLogin) http.ListenAndServe(":8080", mux) }
Github
The github OAuth example are inspired for this blog
First, we make sure our application is registered in our github account. Go at https://github.com/settings/developers and register a new application.
Now we setup our environment variables and we can run the code bellow.
We use Client ID and Client Secret to connect to your github account.
package main import ( "fmt" "github.com/google/go-github/github" "golang.org/x/oauth2" githuboauth "golang.org/x/oauth2/github" "net/http" "os" ) var ( // You must register the app at https://github.com/settings/applications // Set callback to http://127.0.0.1:7000/github_oauth_cb // Set ClientId and ClientSecret to oauthConf = &oauth2.Config{ ClientID: os.Getenv("ENV_GITHUB_CLIENT_ID"), ClientSecret: os.Getenv("ENV_GITHUB_CLIENT_SECRET"), Scopes: []string{"user:email", "repo"}, //RedirectURL: "http://localhost:7000/auth/github/callback", Endpoint: githuboauth.Endpoint, } // random string for oauth2 API calls to protect against CSRF oauthStateString = "thisshouldberandom" ) const htmlIndex = `<html><body> Logged in with <a href="/login">GitHub</a> </body></html> ` // / func handleMain(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write([]byte(htmlIndex)) } // /login func handleGitHubLogin(w http.ResponseWriter, r *http.Request) { url := oauthConf.AuthCodeURL(oauthStateString, oauth2.AccessTypeOnline) http.Redirect(w, r, url, http.StatusTemporaryRedirect) } // /github_oauth_cb. Called by github after authorization is granted func handleGitHubCallback(w http.ResponseWriter, r *http.Request) { state := r.FormValue("state") if state != oauthStateString { fmt.Printf("invalid oauth state, expected '%s', got '%s'\n", oauthStateString, state) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) return } code := r.FormValue("code") token, err := oauthConf.Exchange(oauth2.NoContext, code) if err != nil { fmt.Printf("oauthConf.Exchange() failed with '%s'\n", err) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) return } oauthClient := oauthConf.Client(oauth2.NoContext, token) client := github.NewClient(oauthClient) user, _, err := client.Users.Get("") if err != nil { fmt.Printf("client.Users.Get() faled with '%s'\n", err) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) return } fmt.Printf("Logged in as GitHub user: %s\n", *user.Login) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) } func main() { http.HandleFunc("/", handleMain) http.HandleFunc("/login", handleGitHubLogin) http.HandleFunc("/github_oauth_cb", handleGitHubCallback) fmt.Print("Started running on http://127.0.0.1:7000\n") fmt.Println(http.ListenAndServe(":7000", nil)) }
For twitter, you can find the next example in this excellent blog post. Twitter implements OAuth 1.0a but Go standard library does not have support for this authentication method. The blog post I linked up explain everything you need to know to connect to Twitter.
The sample bellow uses the mrjones/oauth package to connect to Twitter
package main import ( "fmt" "github.com/mrjones/oauth" "io/ioutil" "log" "os" ) var ( ConsumerKey = os.Getenv("ENV_TWITTER_CONSUMER_KEY") ConsumerSecret = os.Getenv("ENV_TWITTER_CONSUMER_SECRET") AccessToken = os.Getenv("ENV_TWITTER_ACCESS_TOKEN") AccessTokenSecret = os.Getenv("ENV_TWITTER_ACCESS_TOKEN_SECRET") ) func main() { consumer := oauth.NewConsumer(ConsumerKey, ConsumerSecret, oauth.ServiceProvider{}) //NOTE: remove this line or turn off Debug if you don't //want to see what the headers look like consumer.Debug(true) //Roll your own AccessToken struct accessToken := &oauth.AccessToken{Token: AccessToken, Secret: AccessTokenSecret} twitterEndPoint := "https://api.twitter.com/1.1/statuses/mentions_timeline.json" response, err := consumer.Get(twitterEndPoint, nil, accessToken) if err != nil { log.Fatal(err, response) } defer response.Body.Close() fmt.Println("Response:", response.StatusCode, response.Status) respBody, err := ioutil.ReadAll(response.Body) if err != nil { log.Fatal(err) } fmt.Println(string(respBody)) }
Google Plus
Google Plus has a repository to demonstrate how to use google plus OAuth authentication here. I will not copy this code here so go and check it out.
Conclusion
We could connect to many mainstream platforms using Go (Golang) and the OAuth protocol. The process is pretty easy and you do not have to depend on many third-party libraries.