Tamizh in words

Leveraging interfaces in golang - Part 1

Published on
Read Time
ยท 6 min read

In my previous blog post on using golang in production, I have mentioned that interfaces are my favorite feature in golang.

As a follow-up of this comment, I would like to share how we are using (my current project is also in golang!) the interfaces to keep our code clean and consistent through a series of three blog posts

This blog post series assumes that you are familiar with the basics of interfaces in golang. If would like to know what it brings to the table, I strongly recommend to check out this well-written article by Yan Cui.

Let me start the series with a solution that we have implemented a few days back.

Some Context

The product that we are building consists of a suite of web applications. To authenticate the users, these web applications talk to a centralized web application called "Identity Server".

Upon receiving valid login credentials, the Identity Server generates a JSON Web Token(JWT) with the corresponding user claims and signs it using a public/private key pair using RSA.

The downstream web applications will then use this JWT to grant the access to their corresponding protected resources.

Let's assume a simple JWT claims payload

{
"sub": "1234567890",
"name": "John D",
"admin": true
}

The above payload uses one of the JWT standard claims, sub, to communicate the unique identifier of the user in the product for all the web applications. The name and admin are the custom claims.

As per the JWT spec, the sub claim is a case-sensitive string containing a StringOrURI value which is locally unique in the context of the issuer or be globally unique, but in our system, the unique identifier of a user is an unsigned integer(uint) type.

As we will be sharing the JWT with other third party applications, we made a call to stick to the JWT spec and converted the uint to string type while generating the token and did the reverse while authenticating the user using this token.

Update - After writing this blog post, I came to know from the comment that the use case of this blog post, unmarshalling a JSON string to uint type can be done by adding string to the json tag. Being said that, if you'd like to know about how to use an interface to solve it, the rest of the post would help.

Unmarshalling JWT - A Naive Approach

Before witnessing the golang interface in action, let's see a naive implementation how we can unmarshal the claim and use.

The straightforward thing would be creating a struct matching the properties of the claim

type UserJwt struct {
Sub string
Name string
Admin bool
}

and unmarshalling using the json package in golang

claims := `
{
"sub": "1234567890",
"name": "John D",
"admin": true
}`


var userJwt *UserJwt
err := json.Unmarshal([]byte(claims), &userJwt)

To convert the sub from string to uint, we can have a method Id on the UserJwt type.

func (u *UserJwt) Id() (uint, error) {
v, err := strconv.ParseUint(u.Sub, 10, strconv.IntSize)
if err != nil {
return 0, err
}
return uint(v), nil
}

and use it after successful unmarshalling of the JWT claims.

// ...
err := json.Unmarshal([]byte(claims), &userJwt)
if err != nil {
return err
}
id, err := userJwt.Id()
if err != nil {
return err
}
// Do something with the id of type uint
fmt.Println(id)

That's great. But did you see any code smell here?

Let me share what's wrong with this approach,

Let's say that we have the following JSON claim with sub has the value user/john instead of a string representing the unsigned integer identifier of the user.

claims := `
{
"sub": "user/john",
"name": "John D",
"admin": true
}`

Unmarshalling this claim will work, and it won't return any error

// ...
err := json.Unmarshal([]byte(claims), &userJwt)
if err != nil {
return err
}

We can share the unmarshalled userJwt with the rest of the code to carry the business logic.

We will come to know that the claim has an invalid sub value only when we try to get the id of the user by calling the Id method

// ...
id, err := userJwt.Id()
// This will return the following error
// strconv.ParseUint: parsing "name/john": invalid syntax

If we didn't call the Id method, this subtle bug slip silently into the product and someday will end up as a production issue!

In a nutshell, this approach is not robust, and the resulting code is not clean.

Using Interfaces - A better approach

Ideally, we want the json.Unmarshal function should return an error if the sub doesn't contain a uint value as string.

To make it happen, we need to inform the json.Unmarshal function somehow to do the type conversion while unmarshalling and return an error if the conversion fails.

How to make it happen?

We can do this by using the Unmarshaler interface.

In our case, we can declare the UnmarshalJSON method with the UserJwt type and in the definition, we can do the type conversion. But that'd be an overkill as we need to do the unmarshalling of the other fields, Name, and Admin, which is already working well without any custom logic.

In other words, the effective way would be overriding the JSON unmarshalling behavior of Sub field alone by having the UnmarshalJSON method with uint type. But according to golang's spec we can't do it

You can only declare a method with a receiver whose type is defined in the same package as the method. You cannot declare a method with a receiver whose type is defined in another package (which includes the built-in types such as int).

To handle this kind of scenario, we can make use of the named types in golang and define a new type called Sub with an underlying type uint

type Sub uint

Then we can declare the UnmarshalJSON method with this Sub type.

func (s *Sub) UnmarshalJSON(b []byte) error {
sub := strings.Replace(string(b), `"`, "", 2)
v, err := strconv.ParseUint(sub, 10, strconv.IntSize)
if err != nil {
return err
}
*s = Sub(uint(v))
return nil
}

Using the Replace function, we are getting rid of the double quotes in the actual JSON encoded value

With this new Sub type in place, We can rewrite the UserJwt by replacing the Sub field with the Id field of type Sub.

type UserJwt struct {
Id Sub `json:"sub"`
Name string
Admin bool
}

The json tag with the value "sub" is required to map the sub key in the JSON claim with the Id.

Now if we try to unmarshal the invalid claim, the json.Unmarshal function will return an error

claims := `
{
"sub": "user/john",
"name": "John D",
"admin": true
}`

var userJwt *UserJwt
err := json.Unmarshal([]byte(claims), &userJwt)
// This will return the following error
// strconv.ParseUint: parsing "name/john": invalid syntax

For a valid claim, we can now get the Id directly from the UserJwt Id field.

claims := `
{
"sub": "1234567890",
"name": "John D",
"admin": true
}`

var userJwt *UserJwt
err := json.Unmarshal([]byte(claims), &userJwt)
// ...
fmt.Println(userJwt.Id)

That's it! The code is in better shape now ๐Ÿ˜ƒ

Summary

In this blog post, we have seen how we can write cleaner code by leveraging the interfaces. The source code is available in my GitHub repository.


Did the content capture your interest? Stay in the loop by subscribing to the RSS feed and staying informed!