In this post we will see how we can implement OAuth2 authentication in a Go web application.

We will create a working website that can allow a user to sign in using Github authentication.

banner

If you just want to see the code, you can view the full example on Github

How OAuth2 Works

Let’s take a brief look at the OAuth protocol before we jump into implementation.

If you’ve ever seen a dialog like this, then you’ve probably used OAuth before:

gitlab using github oauth

Here, we are trying to login to Gitlab using Github to authenticate.

There are three parties in any OAuth mechanism:

  1. The client - The person, or user who is trying to log in
  2. The consumer - The application that the client wants to log into (which is Gitlab in this example)
  3. The service provider - The external application that authenticates the users identity. (which is Github in this example)

In this post, we’ll create a Go HTTP server (consumer) that uses Github’s OAuth2 API (service provider) to authenticate the user (client).

Let’s look at an overview of how this would work in practice.

oauth flow diagram

Let’s look at how to implement each part:

Creating the Landing Page

Lets create the first part of the application, which is the landing page. This will be a basic HTML page, with a link that the user can click on to authenticate with Github.

We can create a new file, public/index.html:

<!DOCTYPE html>
<html>
  <body>
    <a
      href="https://github.com/login/oauth/authorize?client_id=myclientid123&redirect_uri=http://localhost:8080/oauth/redirect"
    >
      Login with github
    </a>
  </body>
</html>

The link URL has three key parts:

  • https//github.com/login/oauth/authorize is the OAuth gateway for Github’s OAuth flow. All OAuth providers have a gateway URL that you have to send the user to in order to proceed.

  • client_id=myclientid123 - this specifies the client ID of the application. This ID will tell Github about the identity of the consumer who is trying to use their OAuth service.

    OAuth service providers normally have a portal in which you can register your consumer. On registration, you will receive a client ID (which we are using here as myclientid123), and a client secret (which we will use later on). For Github, the portal to register new applications can be found on https://github.com/settings/applications/new.

  • redirect_uri=http://localhost:8080/oauth/redirect - specifies the URL to redirect to with the request token, once the user has been authenticated by the service provider. Normally, you will have to set this value on the registration portal as well, to prevent anyone from setting malicious callback URLs.

Next, we need to create the HTTP server to serve the index.html file we just made.

The following code would go into a new file main.go:

func main() {
	fs := http.FileServer(http.Dir("public"))
	http.Handle("/", fs)

	http.ListenAndServe(":8080", nil)
}

You can start the server by executing go run main.go and visit http://localhost:8080: You will see the landing page we just made.

Once you click on the “Login with github” link, you will be redirected to the familiar OAuth page to register with Github. However, once you authenticate, you will be redirected to http://localhost:8080/oauth/redirect, which will lead to a 404 page on the server.

Let’s implement it now:

Adding a Redirect Route

Once the user authenticates with Github, they get redirected to the redirect URL that was specified earlier.

The service provider also adds a request token along with the url. In this case, Github adds this as the code parameter, so the redirect URL will actually be something like http://localhost:8080/oauth/redirect?code=mycode123, where mycode123 is the request token.

We need this request token and our client secret to get the access token, which is the token that is actually used to get information about the user. We get this access token by making a POST HTTP call to https://github.com/login/oauth/access_token along with the mentioned information.

You can view the documentation page for the details of the information Github provides to the redirect URL, and the information we need for provide with the POST /login/oauth/access_token HTTP call.

Let’s add the /oauth/redirect route to the main.go file:

const clientID = "<your client id>"
const clientSecret = "<your client secret>"

func main() {
	fs := http.FileServer(http.Dir("public"))
	http.Handle("/", fs)

	// We will be using `httpClient` to make external HTTP requests later in our code
	httpClient := http.Client{}

	// Create a new redirect route route
	http.HandleFunc("/oauth/redirect", func(w http.ResponseWriter, r *http.Request) {
		// First, we need to get the value of the `code` query param
		err := r.ParseForm()
		if err != nil {
			fmt.Fprintf(os.Stdout, "could not parse query: %v", err)
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		code := r.FormValue("code")

		// Next, lets for the HTTP request to call the github oauth endpoint
		// to get our access token
		reqURL := fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", clientID, clientSecret, code)
		req, err := http.NewRequest(http.MethodPost, reqURL, nil)
		if err != nil {
			fmt.Fprintf(os.Stdout, "could not create HTTP request: %v", err)
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		// We set this header since we want the response
		// as JSON
		req.Header.Set("accept", "application/json")

		// Send out the HTTP request
		res, err := httpClient.Do(req)
		if err != nil {
			fmt.Fprintf(os.Stdout, "could not send HTTP request: %v", err)
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		defer res.Body.Close()

		// Parse the request body into the `OAuthAccessResponse` struct
		var t OAuthAccessResponse
		if err := json.NewDecoder(res.Body).Decode(&t); err != nil {
			fmt.Fprintf(os.Stdout, "could not parse JSON response: %v", err)
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		// Finally, send a response to redirect the user to the "welcome" page
		// with the access token
		w.Header().Set("Location", "/welcome.html?access_token="+t.AccessToken)
		w.WriteHeader(http.StatusFound)
	})

	http.ListenAndServe(":8080", nil)
}

type OAuthAccessResponse struct {
	AccessToken string `json:"access_token"`
}

Now the redirect URL is functional, and will redirect the user to the welcome page, along with the access token.

Redirecting to the Welcome Page

The welcome page is the page we show the user after they have logged in. Now that we have the users access token, we can obtain their account information on their behalf as authorized Github users.

For a list of all APIs available, you can see the Github API Documentation

We will be using the /user API to get basic info about the user and say hi to them on our welcome page. Create a new file public/welcome.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Hello</title>
  </head>

  <body></body>
  <script>
    // We can get the token from the "access_token" query
    // param, available in the browsers "location" global
    const query = window.location.search.substring(1);
    const token = query.split("access_token=")[1];

    // Call the user info API using the fetch browser library
    fetch("https://api.github.com/user", {
      headers: {
        // This header informs the Github API about the API version
        Accept: "application/vnd.github.v3+json",
        // Include the token in the Authorization header
        Authorization: "token " + token,
      },
    })
      // Parse the response as JSON
      .then((res) => res.json())
      .then((res) => {
        // Once we get the response (which has many fields)
        // Documented here: https://developer.github.com/v3/users/#get-the-authenticated-user
        // Write "Welcome <user name>" to the documents body
        const nameNode = document.createTextNode(`Welcome, ${res.name}`);
        document.body.appendChild(nameNode);
      });
  </script>
</html>

With the addition of the welcome page, our OAuth implementation is now complete!

Once the app starts, we can visit http://localhost:8080/ , authorize with Github, and end up on the welcome page, which displays the greeting. My name on my github profile is “Soham Kamani”, so the welcome page will display Welcome, Soham Kamani once I login.

oauth flow

You can find the complete source code for this example on Github

Additional Security Measures

Although this post demonstrated the basics of OAuth2, there is a lot more that can be done to further secure your application.

Session Tokens

In this example, we passed the access token to the client so that it can make requests as the authorized user. To make your app more secure, the access token should not be passed directly to the user. Instead, create a session token that is sent to the user as a cookie.

The app will maintain a mapping of the session tokens to access tokens on the server side (most likely a database).

Instead of making requests to github, the user will make requests to the node server (with the session token), which will in turn use the provided session token to look up the access token and make the request to github on the server side.

session token is mapped to the access token in a database and used to return user info

I have written more about sessions and cookies here.

OAuth Query State

While sending the user to the authorization URL, there is a provision to provide a value for a query parameter called state. The value of this should be a random un-guessable string provided by the application.

When github calls the redirect url, it will attach this state variable to the request params. The new URL would now look like:

https://github.com/login/oauth/authorize?client_id=myclientid123&redirect_uri=http://localhost:8080/oauth/redirect&state=somerandomstring

The application can now compare this value with the value it originally generated. If they are not the same, that means the request came from some third party, and should be rejected.

the redirect URL now contains a state that the client can compare with localStorage

This helps protect our application against cross site request forgery attacks.