When I created mouthful, I was intending it to be rather light and not feature rich but after getting a few feature requests getting in, I’ve decided to expand it. One of the issues was a request to reuse logon credentials for the admin panel. For that, I’ve needed OAuth. I did not have much prior experience with OAuth, so it did intimidate me a bit. However, after implementing OAuth for mouthful, I can say that nowadays - it’s rather easy including OAuth in your applications as well. It’s also a rather good idea to do so as people behind the providers such as github or facebook are probably going to do a better job than a lone developer like me will at securing your credentials. Anyway, with this post I’d like to show how easy it is to add OAuth to your gin project.
The start
Let’s start with a basic gin app, straight from the gin examples. One thing I’ll change is the default route. Instead of the default ping in the demo, we’ll serve some html.
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
htmlFormat := `<html><body>%v</body></html>`
html := fmt.Sprintf(htmlFormat, "Test")
r.GET("/", func(c *gin.Context) {
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html))
})
r.Run() // listen and serve on 0.0.0.0:8080
}
Understanding OAuth with goth
Now the pure magic part is the ability to use ready to use libraries for OAuth such as goth. Goth supports a ton of providers, so it means we can have all of them available for us, on our application. For the demo purposes, I’ll stick to a single one though. I’ll use the github provider. Using goth is rather easy. But before we start with that a quick refresher on OAuth. I’ll keep it a bit simplistic. Before OAuth can take place, you need a secret that you and the third party knows. This can be found on the providers webpage. Once you have those, the flow is basically as follows:
- A user initiates the action to log in through a third party provider
- The user is redirected to the 3rd parties provider to agree to giving OAuth access.
- The user agrees and gives you OAuth access.
- The provider then redirects the user back to your website with an auth code.
- With the OAuth code your web server can then gain access and fetch users information.
With that, what we need to enable OAuth in this example is:
- A button to initiate the flow
- An auth endpoint to that the button will take the user to
- A callback that will get called once the auth is done, so we can gain the user info
Let’s start auth endpoint. For this, we’ll set up goth and let it handle all the complexity of OAuth. Here’s how to do it for github.
package main
import (
"fmt"
"net/http"
"os"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/github"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
githubProvider := github.New(os.Getenv("GITHUB_KEY"), os.Getenv("GITHUB_SECRET"), "http://localhost:8080/callback")
goth.UseProviders(githubProvider)
htmlFormat := `<html><body>%v</body></html>`
r.GET("/", func(c *gin.Context) {
html := fmt.Sprintf(htmlFormat, "Test")
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html))
})
r.Run() // listen and serve on 0.0.0.0:8080
}
We will need a github key and a github secret and those can be gotten under developer settings of your account. We also provide a callback address, and that will be used for registering an OAuth application under your github account as well, so do make sure to use the one I’ve added if you’re following along with this.
Now all we need to implement a couple of endpoints, the auth redirect and the callback.
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/github"
)
func main() {
r := gin.Default()
githubProvider := github.New(os.Getenv("GITHUB_KEY"), os.Getenv("GITHUB_SECRET"), "http://localhost:8080/callback")
goth.UseProviders(githubProvider)
htmlFormat := `<html><body>%v</body></html>`
r.GET("/", func(c *gin.Context) {
html := fmt.Sprintf(htmlFormat, "Test")
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html))
})
r.GET("/github", func(c *gin.Context) {
q := c.Request.URL.Query()
q.Add("provider", "github")
c.Request.URL.RawQuery = q.Encode()
gothic.BeginAuthHandler(c.Writer, c.Request)
})
r.GET("/callback", func(c *gin.Context) {
q := c.Request.URL.Query()
q.Add("provider", "github")
c.Request.URL.RawQuery = q.Encode()
user, err := gothic.CompleteUserAuth(c.Writer, c.Request)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
res, err := json.Marshal(user)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
jsonString := string(res)
html := fmt.Sprintf(htmlFormat, jsonString)
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html))
})
r.Run() // listen and serve on 0.0.0.0:8080
}
The /github
endpoint will do the redirect to github. You might be wondering why do I need to manipulate the request by adding the value provider to the query. Well, in all reality - you don’t if you’re using proper rest API practices and working with multiple OAuth providers. Goth uses the query to figure out which of the registered OAuth providers to use to try and initiate the flow. I’m just faking the query via the three lines:
q := c.Request.URL.Query()
q.Add("provider", "github")
c.Request.URL.RawQuery = q.Encode()
I’m also doing the same in the callback, so - ignore them. In a proper, non demo solution this is not needed as you’ll have the provider passed in as a parameter in a route. The meaty parts here are the gothic.BeginAuthHandler(c.Writer, c.Request)
that’s responsible for beginning the OAuth flow and user, err := gothic.CompleteUserAuth(c.Writer, c.Request)
for completing the flow and parsing the user details. I’ve also added the user details as a serialized json to our output html once the flow is complete via the callback.
Now onto the button. All we need to do is redirect the user to the /github
endpoint. I simply change the routers GET /
to the following:
r.GET("/", func(c *gin.Context) {
html := fmt.Sprintf(htmlFormat, `<a href="/github">Login through github</a>`)
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html))
})
With that, our OAuth setup is complete. Run your go application, just be sure to set the environment variables GITHUB_KEY
and GITHUB_SECRET
before hand. You should now be able to log in and see all the details in json format that github provides for your user.
Why do you want this?
In general, handling user credentials is a great responsibility and one should never take it lightly. Therefore it is best to use OAuth if at all possible and not bother making your own Auth services. This allows for greater safety, as it is rather unlikely you’ll come up with a solution more secure than some of the big players. It is rarely a good idea to implement your own Auth solution! With tools like Goth that make this process trivial - there is no excuse for making one yourself.
The full code snippet is also available as a gist.
Had any issues following the guide? Have I gotten anything wrong? Do let me know in the comments below.