Matt White

Matt White

developer

Matt White

Developer

GitHub OAuth2 in Go

I’ve been playing around with Git repository analysis tools recently (my favorite of which is the now-defunct gitinspector), tracking my contributions on a regular basis. These tools can only tell you so much, so I’ve been building out a project to replace my weekly spreadsheet entries. Integrating with GitHub - a treasure trove of contribution data - seemed an inevitability.

Signing up for the GitHub Developer Program is pretty straightforward, so I won’t cover it here. You provide a URL and a support email and poof - you’re a developer.

If you prefer to jump to the end, you can probably get what you need from this post’s accompanying repo.

Key - Matt Artz

We’ll be looking at how to set up a basic OAuth2 flow in Golang (at which I am a beginner). GitHub covers the same ground as this post quite well here, albeit in Ruby.

Luckily, Go already has the oauth2 package at the ready, which you can install with go get golang.org/x/oauth2. The oauth2 package uses a Config to represent the standard OAuth flow.

conf := &oauth2.Config{
    ClientID:     "YOUR_CLIENT_ID",
    ClientSecret: "YOUR_CLIENT_SECRET",
    Scopes:       []string{"SCOPE1", "SCOPE2"},
    Endpoint: oauth2.Endpoint{
        AuthURL:  "https://provider.com/o/oauth2/auth",
        TokenURL: "https://provider.com/o/oauth2/token",
    },
}

This makes it pretty clear what we need in order to authenticate. In our case, both the ClientID and the ClientSecret will come from GitHub, so we’ll need to setup a new OAuth application.

New OAuth Application

You can use any values for the application name and homepage url. For our purposes, use http://localhost:4567/callback for the callback.

Once you’ve created your application, you should be redirected to a page with your own ClientID and ClientSecret. To keep them out of the codebase, let’s store the GitHub credentials in environment variables by exporting them:

export GITHUB_CLIENT_ID=<your GitHub client id>
export GITHUB_CLIENT_SECRET=<your GitHub client secret>

With those in place, we can create our Config:

package main

import (
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/github"
	"os"
)

func main() {
	conf := &oauth2.Config{
		ClientID:     os.Getenv("GITHUB_CLIENT_ID"),
		ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
		Scopes:       []string{"public_repo"},
		Endpoint: github.Endpoint,
	}
}

Here we’re using the included github.Endpoint constant and limiting ourselves to a public_repo scope. GitHub provides a list of available scopes from which you can pick.

From here, it’s surprisingly easy to get integrated. We need to call AuthCodeURL to get the OAuth redirect and we need a basic server to handle the callback and token exchange with GitHub.

	url := conf.AuthCodeURL("", oauth2.AccessTypeOffline)
	fmt.Printf("Login with GitHub: %v", url)


	http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request){
		err := r.ParseForm()
		if err != nil {
			log.Fatal(err)
		}

		token, err := conf.Exchange(ctx, r.Form["code"][0])
		if err != nil {
			log.Fatal(err)
		}

		fmt.Fprintf(w, "Token: %s", token)
	})

	log.Fatal(http.ListenAndServe(":4567", nil))

At this point, you can run go run main.go, click on the link, authenticate with GitHub, and GitHub will callback to your simple server that retrieves your authentication token. But let’s actually do something with the GitHub API.

For my selfish purposes, I want to be interacting with the repository API endpoints. Let’s make a call to the repo list endpoint and display it on the callback page. Altogether:

package main

import (
	"context"
	"fmt"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/github"
	"io/ioutil"
	"log"
	"net/http"
	"os"
)

func main() {
	ctx := context.Background()
	conf := &oauth2.Config{
		ClientID:     os.Getenv("GITHUB_CLIENT_ID"),
		ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
		Scopes:       []string{"public_repo"},
		Endpoint: github.Endpoint,
	}
	url := conf.AuthCodeURL("", oauth2.AccessTypeOffline)
	fmt.Printf("Login with GitHub: %v", url)


	http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request){
		err := r.ParseForm()
		if err != nil {
			log.Fatal(err)
		}

		token, err := conf.Exchange(ctx, r.Form["code"][0])
		if err != nil {
			log.Fatal(err)
		}

		client := conf.Client(ctx, token)
		response, err := client.Get("https://api.github.com/user/repos?page=0&per_page=100")
		if err != nil {
			log.Fatal(err)
		}

		defer response.Body.Close()
		repos, err := ioutil.ReadAll(response.Body)
		if err != nil {
			log.Fatal(err)
		}

		fmt.Fprintf(w, "Repos: %s", repos)
	})

	log.Fatal(http.ListenAndServe(":4567", nil))
}

The response from GitHub is a mess of JSON - which will be far more useful if you deserialize it first.

This is obviously a very bare bones authentication flow. For fleshing out the implementation a bit more, I recommend Gergely Brautigam’s How to do Google sign-in with Go.