As I move forward with my Beego project, I have reached a point where I need to authenticate my users. In the flow that I’m looking for, the client (web app, mobile app, etc…), will communicate directly with the Auth provider (Google, Facebook, etc…) and get a JWT. The only thing the server needs to do is validate the JWT. If the validation succeeds, it means the user is logged in.

If you are not familiar with JWT, you can read my previous article that explains how JWT works.

Authentication flow

Since the authentication with Google is going to happen entirely on the client, the server logic becomes a lot simpler. For my application, all endpoints will require the user to be logged in, so I will create a middleware to verify this. The middleware will expect a valid JWT in the Authorization header. If this requirement is not met, the server will return a 401.

When a request comes with a valid JWT there are two possible scenarios: New user or existing user. To keep track of existing users we will need a database. In the simplest scenario we need three fields in the user table:

  • id – An internal id to be used inside the application
  • issuer – The name of the auth provider. In the case of Google, the value is: accounts.google.com
  • issuer_id – The id given to this user by the issuer

The id field should be a unique key on the user table and will be used for relationships with other tables. issuer_id can be repeated, but the combination of issuer and issuer_id are unique (A JWT with google id of 1234 will always resolve to the same user).

Now that we have our user table ready, we can decide what to do when we receive a valid request. When we receive a request with a valid JWT, we will first query for a user by issuer and issuer_id (this information is in the JWT). If the user is found, you can just proceed using the correct access control for that user. If the user is not found, we add it to the database and then proceed normally.

Creating a middleware

Middlewares in Beego are known as Filters. We can add our filter in our main.go file for now. This is how the main function looks after adding the filter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
    if beego.BConfig.RunMode == "dev" {
        beego.BConfig.WebConfig.DirectoryIndex = true
        beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
    }

    var AuthFilter = func(ctx *context.Context) {
        // If token valid, continue, else return 401
    }

    beego.InsertFilter("/*", beego.BeforeRouter, AuthFilter)

    beego.Run()
}

If you get an error saying that context is not defined, you will need to add this to your imports:

1
"github.com/astaxie/beego/context"

For actually validating the JWT I’m going to use coreos’ go-oidc library. My AuthFilter function ended up looking something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
var AuthFilter = func(ctx *context.Context) {
    // The Authorization header should come in this format: Bearer <jwt>
    // The first thing we do is check that the JWT exists
    header := strings.Split(ctx.Input.Header("Authorization"), " ")
    if len(header) != 2 || header[0] != "Bearer" {
        ctx.Abort(401, "Not authorized")
    }

    // I had to do something hacky here because beego uses its own
    // context instead of the standard one that most libraries use.
    // I imported context with the name netctx:
    // import netctx "context"
    // oidc uses the context to communicate with the auth provider.
    // I just created a new context to satisfy this requirement
    c := netctx.TODO()
    provider, err := oidc.NewProvider(c, "https://accounts.google.com")
    if err != nil {
        ctx.Abort(500, "Could not create google provider")
    }

    var verifier = provider.Verifier(&oidc.Config{ClientID: "<your-client-id>"})

    // Parse and verify ID Token payload.
    parsedToken, err := verifier.Verify(c, header[1])
    if err != nil {
        ctx.Abort(401, "Not authorized")
    }

    // User is valid. We use ReadOrCreate to create a new user or get
    // the id of the user that matches the issuer and issuer_id
    o := orm.NewOrm()
    user := models.User{
        Issuer: parsedToken.Issuer,
        IssuerId: parsedToken.Subject,
    }
    _, id, err := o.ReadOrCreate(&user, "Issuer", "IssuerId")
    if err != nil {
        ctx.Abort(500, "Error retrieving authenticated user from DB")
    }

    // User has been correctly authenticated. Add the id of the user to the
    // context so controllers can use it if neccesary
    ctx.Input.SetData("userId", id)
}

Since we are adding the user id to the context, we can then retrieve it from a controller if needed:

1
controller.Ctx.Input.GetData("userId")

With just a few lines of code we have added authentication to our application.

[ golang  programming  security  ]
Google sign-in with Golang
Dealing with Firestore 10 limit when using IN operator
Introduction to Golang modules
Using testify for Golang tests
Unit testing Golang code