gobuffalo

In the previous part of the tutorial, we've generated resource endpoints for managing skills data to be displayed on a resume page. However, the problem was that anyone who could see the page, might potentially access those endpoints and edit the data. In this post, we'll add a not-so-complicated authentication using an external provider (Github) to restrict access to those pages.

You might want to check out previous parts of this tutorial first:

Authentication needed

When we generated a handler for skills it got added to actions/app.go as well:

// actions/app.go
func App() *buffalo.App {
    if app == nil {
        ...
        app.Resource("/skills", SkillsResource{&buffalo.BaseResource{}})
    }
    return app
}

What happens under the covers is that a new path group was created under /skills and there were some endpoints added to it, such as listing all skills, adding a new one, etc. One of the features that an App.Group provides is adding a specific middleware, without having it in use for all the other paths. This is perfect for our use case since we don't want the users to authenticate to see the public part of the page, but the should do it if they want to edit the content.

Generating auth using goth

Authentication is yet another thing that is so common, that Buffalo has a generator for it. The authors know that you will probably need it at some point, and we've just reached it. The tool that is being used to do the heavy lifting here is goth, and it provides quite a long list of external auth providers including Facebook, Twitter, Google+ or Auth0. In our tutorial, we'll use an integration with Github. To do that we type a command to have stuff generated:

buffalo generate goth github

It created a single file called actions/auth.go and added a couple of lines in actions/app.go. Let's start with the latter:

// actions/app.go
auth := app.Group("/auth")
auth.GET("/{provider}", buffalo.WrapHandlerFunc(gothic.BeginAuthHandler))
auth.GET("/{provider}/callback", AuthCallback)

There are two endpoints added to our application, but we need to look inside actions/auth.go to see what happens inside to fully grasp what those are:

// actions/auth.go
func init() {
    gothic.Store = App().SessionStore

    goth.UseProviders(
        github.New(os.Getenv("GITHUB_KEY"), os.Getenv("GITHUB_SECRET"), fmt.Sprintf("%s%s", App().Host, "/auth/github/callback")),
    )
}

func AuthCallback(c buffalo.Context) error {
    user, err := gothic.CompleteUserAuth(c.Response(), c.Request())
    if err != nil {
        return c.Error(401, err)
    }
    // Do something with the user, maybe register them/sign them in
    return c.Render(200, r.JSON(user))
}

First of all, we have a Github integration added to a list of providers available to goth in an init() function. Looking back at app.go file we can see that an authentication endpoint calls some gothic.BeginAuthHandler function with a provider parameter sent in a path. In our case, we would enter it via /auth/github, so that our sole provider will be chosen. If you dive deeper into goth you can see that it can be passed as a query parameter or request variable (via gorilla/mux and mux.Vars(req)["provider"]). Anyway, what it does is redirects the user to an external provider login page, where they need to authorize to proceed.

In order to set up the integration, we need to create an application on Github as well. In order to do it we go to our settings page, then click on OAuth Apps (under Developer settings submenu on the left), then create an application by clicking on Register a new application button. There we need to specify a name for the app, it's homepage (in our case http://127.0.0.1:3000) and a callback URL. So we now discover what is that second entry added to app.go.

After user authorization on Github side, they need to be redirected back to our application. In order to do that, Github needs to know where should they send our users. This second endpoint is actually defined in actions/auth.go:

// actions/auth.go
func AuthCallback(c buffalo.Context) error {
    user, err := gothic.CompleteUserAuth(c.Response(), c.Request())
    if err != nil {
        return c.Error(401, err)
    }
    // Do something with the user, maybe register them/sign them in
    return c.Render(200, r.JSON(user))
}

It doesn't do a whole lot, it just prints a JSON sent by the provider. Anyway, we need to add a path to that callback endpoint on Github side. Once we do that (and save the app) we get two secrets: Client ID and Client Secret that need to be passed into Buffalo application as GITHUB_KEY and GITHUB_SECRET env variables, respectively. If we set this up, we can finally authenticate ourselves to the application. If we enter /auth/github, we get redirected to Github:

Once we enter our credentials there, we are sent back to our application, and see the JSON with the profile details.

Adding middleware, logout

At this point, we should process that profile information, create some user profile of our own and bind those two together. We won't do that since it might be beyond the scope of a simple tutorial. At this point, we'll settle for a super-naive implementation, where we'll just add a token value to the session and check if it's present. If so, the user can access restricted pages. It not, they'll get redirected to login page.

To check if the user has a token, we introduce a middleware function that checks a value from context's Session and does proper redirects:

// app/auth.go
func IsAuth() buffalo.MiddlewareFunc {
    return func(h buffalo.Handler) buffalo.Handler {
        return func(c buffalo.Context) error {
            t := c.Session().Get("token")
            if t == nil {
                return c.Redirect(http.StatusFound, "/auth/github")
            }
            return h(c)
        }
    }
}

Simple enough, now we need to add this token on authentication callback:

// app/auth.go
func AuthCallback(c buffalo.Context) error {
    user, err := gothic.CompleteUserAuth(c.Response(), c.Request())
    ...
    c.Session().Set("token", user.AccessToken)
    ...
    return c.Render(200, r.JSON(user))
}

Finally, we might want to add a logout link as well. To do that, we just remove the token from a session, forcing users to authenticate with Github again. We can do that using Session.Delete(..) function:

// app/auth.go
func AuthLogout(c buffalo.Context) error {
    c.Session().Delete("token")
    return c.Redirect(http.StatusFound, "/")
}

Finally, we can add our authentication middleware to the pages we want to have a authorized-only access:

// app/auth.go
...
app.Resource("/skills", SkillsResource{&buffalo.BaseResource{}}).Use(IsAuth())
...
auth.GET("/logout", AuthLogout)

We've successfully added endpoints to login and logout our users! One last step would be to redirect users to a correct page after login. We don't want them to see their profile JSON, but actually, access the page they requested before being redirected to Github. To do that let's set another property in Session (called _login_redirectto):

// actions/auth.go
func IsAuth() buffalo.MiddlewareFunc {
    return func(h buffalo.Handler) buffalo.Handler {
        return func(c buffalo.Context) error {
            t := c.Session().Get("token")
            if t == nil {
                // set the redirect URL
                c.Session().Set("login_redirect_to", c.Request().URL.String())
                return c.Redirect(http.StatusFound, "/auth/github")
            }
            return h(c)
        }
    }
}

Now let's read it after a successfull authorization. We can use Session.GetOnce(..) function which removes the value after reading it, since we won't need it anymore:

// actions/auth.go
func AuthCallback(c buffalo.Context) error {
    user, err := gothic.CompleteUserAuth(c.Response(), c.Request())
    if err != nil {
        return c.Error(401, err)
    }

    c.Session().Set("token", user.AccessToken)

    rTo := c.Session().GetOnce("login_redirect_to")
    rToStr, ok := rTo.(string)
    if !ok || rToStr == "" {
        rToStr = "/"
    }

    // Do something with the user, maybe register them/sign them in
    return c.Redirect(http.StatusFound, rToStr)
}

It's slightly more complicated since we need to check it that property exists and it is a string (Session attributes are stored as interface{} type). If everything is OK, we should get redirected to a page we requested before.

That's it! We've added highly-not-production-ready authentication to our application! We can (we should) extend it with a proper token validation, by storing them eg. in the database, but for now, we're good to go.

The full source code of this example is available on Github.