Go Oauth2 Tutorial

Elliot Forbes Elliot Forbes ⏰ 6 Minutes 📅 Sep 10, 2018

Welcome fellow coders! In this tutorial, we are going to be taking a look at how you can implement your own OAuth2 Server and client using the go-oauth2/oauth2 package.

This is without a doubt one of the most requested topics from commentors on my YouTube videos and it’s certainly something that I myself find incredibly interesting.

Security is without doubt a very important feature for any public and even private facing service or API and it’s something that you need to pay a lot of attention to in order to get it right.

Note - The full github repository for this tutorial can be found here: TutorialEdge/go-oauth-tutorial

The Theory

So, before we dive into how we can code this up, it’s important to know how it works in the background. Typically, we have a client that will start by making an authorization request to the resource owner. The resource owner then either grants or denies this request.

With this authorization grant the client then passes this to the authorization server which will grant back an access token. It is with this granted access token that our client can then access a protected resource such as an API or a service.

So, with that said, let’s now look at how we can implement our own authorization server using this go-oauth2/oauth2 package.

Note - If you are interested in seeing the RFC that Oauth2 implementations follow, you can find it here: RFC-6749

A Simple Oauth2 Flow

We’ll start off by implementing a really simple server based on the example that they provide within their documentation. When we pass an client id and a client secret to our authorization server it should return us with our access token that’ll look something like this:

{"access_token":"Z_1QUVC5M_EOCESISKW8AQ","expires_in":7200,"scope":"read","token_type":"Bearer"}

So, let’s dive into our server implementation and see if we can decipher what’s going on:

package main

import (
    "log"
    "net/http"
    "net/url"
    "os"

    "github.com/go-session/session"
    "gopkg.in/oauth2.v3/errors"
    "gopkg.in/oauth2.v3/manage"
    "gopkg.in/oauth2.v3/models"
    "gopkg.in/oauth2.v3/server"
    "gopkg.in/oauth2.v3/store"
)

func main() {
    manager := manage.NewDefaultManager()
    // token store
    manager.MustTokenStorage(store.NewMemoryTokenStore())

    clientStore := store.NewClientStore()
    clientStore.Set("222222", &models.Client{
        ID:     "222222",
        Secret: "22222222",
        Domain: "http://localhost:9094",
    })
    manager.MapClientStorage(clientStore)

    srv := server.NewServer(server.NewConfig(), manager)
    srv.SetUserAuthorizationHandler(userAuthorizeHandler)

    srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
        log.Println("Internal Error:", err.Error())
        return
    })

    srv.SetResponseErrorHandler(func(re *errors.Response) {
        log.Println("Response Error:", re.Error.Error())
    })

    http.HandleFunc("/login", loginHandler)
    http.HandleFunc("/auth", authHandler)

    http.HandleFunc("/authorize", func(w http.ResponseWriter, r *http.Request) {
        err := srv.HandleAuthorizeRequest(w, r)
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
        }
    })

    http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
        err := srv.HandleTokenRequest(w, r)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }
    })

    log.Println("Server is running at 9096 port.")
    log.Fatal(http.ListenAndServe(":9096", nil))
}

func userAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) {
    store, err := session.Start(nil, w, r)
    if err != nil {
        return
    }

    uid, ok := store.Get("UserID")
    if !ok {
        if r.Form == nil {
            r.ParseForm()
        }
        store.Set("ReturnUri", r.Form)
        store.Save()

        w.Header().Set("Location", "/login")
        w.WriteHeader(http.StatusFound)
        return
    }
    userID = uid.(string)
    store.Delete("UserID")
    store.Save()
    return
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    store, err := session.Start(nil, w, r)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    if r.Method == "POST" {
        store.Set("LoggedInUserID", "000000")
        store.Save()

        w.Header().Set("Location", "/auth")
        w.WriteHeader(http.StatusFound)
        return
    }
    outputHTML(w, r, "static/login.html")
}

func authHandler(w http.ResponseWriter, r *http.Request) {
    store, err := session.Start(nil, w, r)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    if _, ok := store.Get("LoggedInUserID"); !ok {
        w.Header().Set("Location", "/login")
        w.WriteHeader(http.StatusFound)
        return
    }

    if r.Method == "POST" {
        var form url.Values
        if v, ok := store.Get("ReturnUri"); ok {
            form = v.(url.Values)
        }
        u := new(url.URL)
        u.Path = "/authorize"
        u.RawQuery = form.Encode()
        w.Header().Set("Location", u.String())
        w.WriteHeader(http.StatusFound)
        store.Delete("Form")

        if v, ok := store.Get("LoggedInUserID"); ok {
            store.Set("UserID", v)
        }
        store.Save()

        return
    }
    outputHTML(w, r, "static/auth.html")
}

func outputHTML(w http.ResponseWriter, req *http.Request, filename string) {
    file, err := os.Open(filename)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    defer file.Close()
    fi, _ := file.Stat()
    http.ServeContent(w, req, file.Name(), fi.ModTime(), file)
}

Our Client

Now that we have our server implementation done and dusted, we can focus on building up our client. This will use the golang.org/x/oauth2 standard package for authenticating.

We’ll be defining a really simple server using net/http which features 2 endpoints:

  • / - The root or homepage of our client
  • /oauth2 - The route which successfully authenticated clients will be automatically redirected to.

We’ll start by defining our oauth2.Config{} object which will contain our ClientID or ClientSecret. Our OAuth2 server implementation already has a note of these two variables and should they not match, we won’t be able to retrieve access tokens from our server.

It’ll also take in a string of Scopes which define the scope of our access token, these scopes can define various different levels of access to a given resource. For example, we could provide define a Read-Only scope which just provides the client read-only access to our underlying resource.

Next, we define the RedirectURL which specifies an endpoint that our Authorization server should redirect to upon successful authentication. We’ll want this handled by our /oauth2 endpoint.

Finally, we specify oauth2.Endpoint which takes in the AuthURL and TokenURL that will point towards our authorization and token endpoints that we defined previously on our server.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"

    "golang.org/x/oauth2"
)

var (
    config = oauth2.Config{
        ClientID:     "222222",
        ClientSecret: "22222222",
        Scopes:       []string{"all"},
        RedirectURL:  "http://localhost:9094/oauth2",
        // This points to our Authorization Server
        // if our Client ID and Client Secret are valid
        // it will attempt to authorize our user
        Endpoint: oauth2.Endpoint{
            AuthURL:  "http://localhost:9096/authorize",
            TokenURL: "http://localhost:9096/token",
        },
    }
)

// Homepage
func HomePage(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Homepage Hit!")
    u := config.AuthCodeURL("xyz")
    http.Redirect(w, r, u, http.StatusFound)
}

// Authorize
func Authorize(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    state := r.Form.Get("state")
    if state != "xyz" {
        http.Error(w, "State invalid", http.StatusBadRequest)
        return
    }

    code := r.Form.Get("code")
    if code == "" {
        http.Error(w, "Code not found", http.StatusBadRequest)
        return
    }

    token, err := config.Exchange(context.Background(), code)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    e := json.NewEncoder(w)
    e.SetIndent("", "  ")
    e.Encode(*token)
}

func main() {

    // 1 - We attempt to hit our Homepage route
    // if we attempt to hit this unauthenticated, it
    // will automatically redirect to our Auth
    // server and prompt for login credentials
    http.HandleFunc("/", HomePage)

    // 2 - This displays our state, code and
    // token and expiry time that we get back
    // from our Authorization server
    http.HandleFunc("/oauth2", Authorize)

    // 3 - We start up our Client on port 9094
    log.Println("Client is running at 9094 port.")
    log.Fatal(http.ListenAndServe(":9094", nil))
}

So, we’ve managed to build up our client. Let’s try and run this and see what happens.

$ go run main.go
2018/10/20 13:25:22 Client is running at 9094 port.

Now, whenever you hit localhost:9094 within your browser, you should see it automatically redirect to your running server implementation, localhost:9096/login. We’ll then provide our credentials admin and admin for demonstration purposes, and this will prompt us to grant access to our client.

When we click Allow it will automatically redirect us back to our Client application /oauth2 endpoint, but it will return a JSON string containing our access_token, refresh_token, token_type and when our tokens will expire.

Awesome, we have a fully working Oauth2 flow implemented.

Conclusion

So, in this tutorial, we looked at how you could implement your own authorization server in Go. We then looked at how we could build a simple Go-based client that could subsequently make requests for access tokens to this server.

Hopefully, you found this tutorial useful! If you did then please feel free to let me know in the comments section below!