Go Oauth2 Tutorial
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!