Hello everyone!
In the previous lecture, we have learned about token based authentication and why PASETO is better than JWT in term of security practice.
Today we will learn how to implement both of them in Golang to see why PASETO is also much easier and simpler to implement compared to JWT.
Here's:
- Link to the full series playlist on Youtube
- And its Github repository
Declare token Maker interface
Alright, let’s start!
First, I’m gonna create a new package called token
. Then create a new file maker.go
inside this package.
The idea is to declare a general token.Maker
interface to manage the creation and verification of the tokens. Then later, we will write a JWTMaker
and a PasetoMaker
struct that implements this interface. By doing so, we can easily switch between different types of token makers whenever we want.
So this interface will have 2 methods:
type Maker interface {
CreateToken(username string, duration time.Duration) (string, error)
VerifyToken(token string) (*Payload, error)
}
The CreateToken()
method takes a username
string and a valid duration
as input. It returns a signed token string or an error. Basically, this method will create and sign a new token for a specific username and valid duration.
The second method is VerifyToken()
, which takes a token string as input, and returns a Payload
object or an error. We will declare this Playload
struct in a moment. The role of this VerifyToken()
method is to checks if the input token is valid or not. If it is valid, the method will return the payload data stored inside the body of the token.
Declare token Payload struct
OK, now let’s create a new payload.go
file, and define the Payload
struct inside it. This struct will contain the payload data of the token.
The most important field is Username
, which is used to identify the token owner.
Then an IssuedAt
field to know when the token is created.
When using token based authentication, it’s crucial to make sure that each access token only has a short valid duration. So we need an ExpiredAt
field to store the time at which the token will be expired.
type Payload struct {
ID uuid.UUID `json:"id"`
Username string `json:"username"`
IssuedAt time.Time `json:"issued_at"`
ExpiredAt time.Time `json:"expired_at"`
}
Normally these 3 fields should be enough. However, if we want to have a mechanism to invalidate some specific tokens in case they are leaked, we need to add an ID
field to uniquely identify each token.
Here I use the UUID type for this field. The type is defined in the google/uuid package, wo we have to run go get
command to download and add it to the project.
go get github.com/google/uuid
Next, I’m gonna define a function NewPayload()
that takes a username
and a duration
as input arguments, and returns a Payload
object or an error. This function will create a new token payload with a specific username and duration.
func NewPayload(username string, duration time.Duration) (*Payload, error) {
tokenID, err := uuid.NewRandom()
if err != nil {
return nil, err
}
payload := &Payload{
ID: tokenID,
Username: username,
IssuedAt: time.Now(),
ExpiredAt: time.Now().Add(duration),
}
return payload, nil
}
First, we have to call uuid.NewRandom()
to generate a unique token ID. If an error occurs, we simply return a nil payload and the error itself.
Else, we create the payload, where ID
is the generated random token UUID
, Username
is the input username
, IssuedAt
is time.Now()
, and ExpiredAt
is time.Now().Add(duration)
.
Then we just return this payload object and a nil
error. And we’re done!
Implement JWT Maker
Now we’re gonna implement a JWTMaker
. We will need a JWT package for Golang, so let’s open the browser and search for jwt golang
.
There might be many different packages, but I think this one is the most popular: https://github.com/dgrijalva/jwt-go. So let’s copy its URL, and run go get
in the terminal to install the package:
go get github.com/dgrijalva/jwt-go
OK, the package is installed. Now let’s go back to visual studio code.
I’m gonna create a new file jwt_maker.go
inside the token
package. Then declare a new type JWTMaker
struct. This struct is a JSON web token maker, which implements the token.Maker
interface.
In this tutorial, I will use symmetric key algorithm to sign the tokens, so this struct will have a field to store the secret key.
type JWTMaker struct {
secretKey string
}
Next, let’s add a function NewJWTMaker()
that takes a secretKey
string as input, and returns a token.Maker
interface, or an error
as output.
By returning the interface, we will make sure that our JWTMaker
must implement the token.Maker
interface. We will see how the go compiler checks this for us in a moment.
Now, although the algorithm we’re gonna use doesn’t require how long the secret key should be, it’s still a good idea to ensure that the key should not be too short, for better security. So I will declare a constant minSecretKeySize = 32
characters.
const minSecretKeySize = 32
func NewJWTMaker(secretKey string) (Maker, error) {
if len(secretKey) < minSecretKeySize {
return nil, fmt.Errorf("invalid key size: must be at least %d characters", minSecretKeySize)
}
return &JWTMaker{secretKey}, nil
}
Then inside this function, we check if the length of the secret key is less than minSecretKeySize
or not. If it is, we just return a nil
object and an error saying that the key must have at least 32 characters.
Otherwise, we return a new JWTMaker
object with the input secretKey
, and a nil
error.
Now you can see a red line here, because the JWTMaker
object that we’ve created doesn’t implement the required methods of the token.Maker
interface, which is the return type of this function.
So in order to fix this, we have to add the CreateToken()
and VerifyToken()
methods to this struct.
Let’s copy them from the token.Maker
interface, and paste them here. Then let’s add the JWTMaker
receiver in front of each method.
func (maker *JWTMaker) CreateToken(username string, duration time.Duration) (string, error) {}
func (maker *JWTMaker) VerifyToken(token string) (*Payload, error) {}
OK, now the red line is gone! Let’s implement the CreateToken()
method!
Implement the JWT CreateToken method
First we create a new token payload by calling NewPayload()
, and pass in the input username
and valid duration
.
If error is not nil, we return an empty token string and the error. Else, we create a new jwtToken
by calling the jwt.NewWithClaims()
function of the jwt-go package.
This function expects 2 input arguments:
- First, the signing method (or algorithm). I’m gonna use
HS256
in this case. - Then the claims, which actually is our created payload.
func (maker *JWTMaker) CreateToken(username string, duration time.Duration) (string, error) {
payload, err := NewPayload(username, duration)
if err != nil {
return "", err
}
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
return jwtToken.SignedString([]byte(maker.secretKey))
}
Finally, to generate a token string, we call jwtToken.SignedString()
, and pass in the secretKey
after converting it to []byte
slice.
Here we have an error because our Payload
struct doesn’t implement the jwt.Claims
interface. It’s missing one method called Valid()
.
The jwt-go package needs this method to check if the token payload is valid or not. So let’s open the payload.go
to add this method.
The signature of this method is very simple. It doesn’t take any input argument, and only return an error in case the token is invalid. You can easily find this method in the implementation of the jwt-go package.
var ErrExpiredToken = errors.New("token has expired")
func (payload *Payload) Valid() error {
if time.Now().After(payload.ExpiredAt) {
return ErrExpiredToken
}
return nil
}
The simplest but also the most important thing we must check is the expiration time of the token.
If time.Now()
is after the payload.ExpiredAt
, then it means that the token has expired. So we just return a new error saying: token has expired.
We should declare this error as a public constant: ErrExpiredToken
, so that we can check the error type from outside.
If the token is not expired, then we simply return nil
. And that’s it! The Valid function is done.
Now back to our jwt_maker.go
file, we can see that the red line on the payload object is gone.
As we’ve imported the jwt-go
package, we should run go mod tidy
in the terminal to add it to the go.mod
file.
module github.com/techschool/simplebank
go 1.15
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-gonic/gin v1.6.3
github.com/go-playground/validator/v10 v10.4.1
github.com/golang/mock v1.4.4
github.com/google/uuid v1.1.4
github.com/lib/pq v1.9.0
github.com/o1egl/paseto v1.0.0
github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.6.1
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
)
The jwt-go version I’m using is 3.2.0
. In the future, it might be possible that you will use a newer version, such as 4.0
, then the function and interface signatures might be different. However, the idea should be similar.
OK, now the CreateToken()
method is done. Let’s move on to the VerifyToken()
method.
Implement the JWT VerifyToken method
This will be a bit more complicated. First, we have to parse the token by calling jwt.ParseWithClaims
and pass in the input token
string, an empty Payload
object, and a key function.
What is a key function? Well, basically, it’s a function that receives the parsed but unverified token. You should verify its header to make sure that the signing algorithm matches with what you normally use to sign the tokens.
Then if it matches, you return the key so that jwt-go can use it to verify the token. This step is very important to prevent the trivial attack mechanism as I explained in the previous lecture.
Alright, I’m gonna copy this function signature, and paste it here. Let’s set this input argument’s name to token
, and its type should be jwt.Token
. Then we just pass the keyFunc to the ParseWithClaims()
call.
func (maker *JWTMaker) VerifyToken(token string) (*Payload, error) {
keyFunc := func(token *jwt.Token) (interface{}, error) {
_, ok := token.Method.(*jwt.SigningMethodHMAC)
if !ok {
return nil, ErrInvalidToken
}
return []byte(maker.secretKey), nil
}
jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc)
...
}
In the key function, we can get its signing algorithm via the token.Method
field. Note that its type is a SigningMethod
, which is just an interface
. So we have to try converting it to a specific implementation.
In our case, we convert it to SigningMethodHMAC
because we’re using HS256
, which is an instance of the SigningMethodHMAC
struct.
This conversion can be successful or not. If it is not ok, then it means that the algorithm of the token doesn’t match with our signing algorithm, so it’s clearly an invalid token.
We have to return a nil
key with an ErrInvalidToken
. I’m gonna declare this new error inside the payload.go
file, the same place as the ErrExpiredToken
. They are different types of error that will be returned by our VerifyToken()
function.
var (
ErrInvalidToken = errors.New("token is invalid")
ErrExpiredToken = errors.New("token has expired")
)
OK, back to the JWTMaker
. If the conversion is successful, then it means the algorithm matches. We can just return the secret key that we’re using to sign the token after converting it to []byte
slice, and a nil error.
OK now the key function is ready. Let’s continue with the ParseWithClaims
function call. If it returns a not nil error, then there might be 2 different scenarios: either the token is invalid or it is expired.
But now things get more complicated when we want to differentiate these 2 cases. If we follow the implementation of the jwt-go package, we can see that it automatically calls token.Claims.Valid()
function under the hood.
And in our implementation of this function, we’re returning ErrExpiredToken
error. However, jwt-go has secretly hiden this original error inside its own ValidationError
object.
So in order to figure out the real error type, we have to convert the returned error of the ParseWithClaims()
function to jwt.ValidationError
func (maker *JWTMaker) VerifyToken(token string) (*Payload, error) {
...
jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc)
if err != nil {
verr, ok := err.(*jwt.ValidationError)
if ok && errors.Is(verr.Inner, ErrExpiredToken) {
return nil, ErrExpiredToken
}
return nil, ErrInvalidToken
}
...
}
Here I assign the converted error to the verr
variable. If the conversion is OK, we use the errors.Is()
function to check if the verr.Inner
is actually the ErrExpiredToken
or not.
If it is, we just return a nil
payload and the ErrExpiredToken
. Otherwise, we just return nil and ErrInvalidToken
.
In case everything is good, and the token is successfully parsed and verified, we will try to get its payload data by converting jwtToken.Claims
into a Payload object.
func (maker *JWTMaker) VerifyToken(token string) (*Payload, error) {
keyFunc := func(token *jwt.Token) (interface{}, error) {
_, ok := token.Method.(*jwt.SigningMethodHMAC)
if !ok {
return nil, ErrInvalidToken
}
return []byte(maker.secretKey), nil
}
jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc)
if err != nil {
verr, ok := err.(*jwt.ValidationError)
if ok && errors.Is(verr.Inner, ErrExpiredToken) {
return nil, ErrExpiredToken
}
return nil, ErrInvalidToken
}
payload, ok := jwtToken.Claims.(*Payload)
if !ok {
return nil, ErrInvalidToken
}
return payload, nil
}
If it’s not OK, then we just return nil
and ErrInvalidToken
. Else, we return the payload object and a nil
error.
And that’s it! The JWTMaker
is completed. Now let’s write some unit test for it!
Test JWT Maker
I’m gonna create a new file jwt_maker_test.go
inside the token
package. Then let’s add a new function TestJWTMaker()
that takes a testing.T
object as input.
First, we have to create a new maker by calling the NewJWTMaker()
function and pass in a random secret key of 32 characters. We require no errors to be returned here.
Next, we generate a username
with the util.RandomOwner()
function, and let’s say the token valid duration
is gonna be 1 minute
.
Let’s also declared 2 variables to compare the result later:
- The
issuedAt
time should betime.Now()
- And we add the
duration
to thisissuedAt
time to get theexpiredAt
time of the token.
func TestJWTMaker(t *testing.T) {
maker, err := NewJWTMaker(util.RandomString(32))
require.NoError(t, err)
username := util.RandomOwner()
duration := time.Minute
issuedAt := time.Now()
expiredAt := issuedAt.Add(duration)
token, err := maker.CreateToken(username, duration)
require.NoError(t, err)
require.NotEmpty(t, token)
payload, err := maker.VerifyToken(token)
require.NoError(t, err)
require.NotEmpty(t, token)
require.NotZero(t, payload.ID)
require.Equal(t, username, payload.Username)
require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second)
require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second)
}
OK, now we generate the token by calling maker.CreatToken
function, and pass in the username
and duration
. Require no errors, and require the output token to not be empty.
Next, we call maker.VerifyToken
to make sure that the token is valid and also get back its payload data. We require no errors, and require the payload object to be not empty.
Then we need to check all fields of the payload object.
- First the
payload.ID
should be not zero. - Then the
payload.Username
should equal to the inputusername
. - We use
require.WithinDuration
to compare thepayload.IssuedAt
field with the expectedissuedAt
time we saved above. They should not be different by more than 1 second. - Likewise, we compare the
payload.ExpiredAt
field with the expectedexpiredAt
time in the same manner.
And we’re done! Let’s run this unit test!
It passed. Cool! So that’s how we test the happy case.
Now let’s add another test to check the expired JWT token case.
Similar as before, we first have to create a new JWTMaker
. Then we will create an expired token by calling maker.CreateToken()
, pass in a random username
and a negative duration
.
func TestExpiredJWTToken(t *testing.T) {
maker, err := NewJWTMaker(util.RandomString(32))
require.NoError(t, err)
token, err := maker.CreateToken(util.RandomOwner(), -time.Minute)
require.NoError(t, err)
require.NotEmpty(t, token)
payload, err := maker.VerifyToken(token)
require.Error(t, err)
require.EqualError(t, err, ErrExpiredToken.Error())
require.Nil(t, payload)
}
We require no errors to be returned, and the token should not be empty. Now we will verify this output token.
This time, we expect an error to be returned. And more specifically, it should be ErrExpiredToken
. Finally, the output payload should be nil
.
OK, let’s run the test!
It passed. Excellent!
The last test we’re gonna write is to check the invalid token case, where a None
algorithm header is used. This is a well-known attack technique that I have told you in the previous lecture.
First I’m gonna create a new payload
with a random username
and a duration
of 1 minute. Require no errors. Then let’s make a new token by calling jwt.NewWithClaims()
with the jwt.SigningMethodNone
and the created payload
.
Now we have to sign this token using the SignedString()
method. But we cannot just use any random secret key here, because the jwt-go library has completely forbidden from using the None
algorithm to sign the token.
We can only use it for testing when we pass in this special constant: jwt.UnsafeAllowNoneSignatureType
as the secret key.
If you follow the implementation of this value, you can see that normally the None sign method is disallowed, unless the input key is this special constant. It basically means that you’re aware of what you’re doing. Make sure you only use it for testing, and not for production.
func TestInvalidJWTTokenAlgNone(t *testing.T) {
payload, err := NewPayload(util.RandomOwner(), time.Minute)
require.NoError(t, err)
jwtToken := jwt.NewWithClaims(jwt.SigningMethodNone, payload)
token, err := jwtToken.SignedString(jwt.UnsafeAllowNoneSignatureType)
require.NoError(t, err)
maker, err := NewJWTMaker(util.RandomString(32))
require.NoError(t, err)
payload, err = maker.VerifyToken(token)
require.Error(t, err)
require.EqualError(t, err, ErrInvalidToken.Error())
require.Nil(t, payload)
}
OK let’s get back to our code. We have to create a new JWTMaker
as in the other tests. And now we call maker.VerifyToken()
to verify the token we’ve signed above.
This time, the function should also return an error, and the error should be equal to ErrInvalidToken
. The output payload should also be nil
.
Alright, now let’s run the test!
It passed! Awesome!
So now you know how to implement and test JWT in go.
Although I think the jwt-go package was quite well implemented in terms of preventing security mistakes, it’s still a bit complicated and difficult to use than necessary, especially for the token verification part.
Implement PASETO Maker
Now I’m gonna show you how to implement the same token maker interface but using PASETO instead. It would be much easier and cleaner than JWT.
OK, let’s open the browser and search for paseto golang
. Open its Github page and copy the URL: https://github.com/o1egl/paseto. Then run go get
with this URL to download the package:
go get github.com/o1egl/paseto
Now get back to our project, I’m gonna create a new file: paseto_maker.go
inside the token
folder.
Similar to what we’ve done with JWT, Let’s declare a type PasetoMaker
struct, which will implement the same token.Maker
interface, but use PASETO instead of JWT.
We’re gonna use the latest version of PASETO at the moment, which is version 2. So the PasetoMaker
struct will have a paseto
field of type paseto.V2
.
type PasetoMaker struct {
paseto *paseto.V2
symmetricKey []byte
}
And as I just want to use the token locally for our banking API, we will use symmetric encryption to encrypt the token payload. Therefore, we need a field to store the symmetricKey
here.
OK now let’s add a function NewPasetoMaker()
, which takes a symmetricKey
string as input, and returns a token.Maker
interface or an error
. This function will create a new PasetoMaker
instance.
Paseto version 2 uses Chacha20 Poly1305 algorithm to encrypt the payload. So here we have to check the length of the symmetric key to make sure that it has the correct size that’s required by the algorithm.
import (
"github.com/aead/chacha20poly1305"
"github.com/o1egl/paseto"
)
func NewPasetoMaker(symmetricKey string) (Maker, error) {
if len(symmetricKey) != chacha20poly1305.KeySize {
return nil, fmt.Errorf("invalid key size: must be exactly %d characters", chacha20poly1305.KeySize)
}
maker := &PasetoMaker{
paseto: paseto.NewV2(),
symmetricKey: []byte(symmetricKey),
}
return maker, nil
}
If the key length is not correct then we just return a nil object and an error saying invalid key size. It must have exactly this number of characters.
Else, we just create a new PasetoMaker object that contains paseto.NewV2()
and the input symmetricKey
converted to []byte
slice.
Then we return this maker
object and a nil
error.
Again, here we see a red line under maker object because it’s not implementing the token.Maker
interface yet. So let’s do the same as what we’ve done for JWTMaker
.
I’m gonna copy these 2 required methods of the token maker interface, and add the PasetoMaker
receiver in front of them.
func (maker *PasetoMaker) CreateToken(username string, duration time.Duration) (string, error) {}
func (maker *PasetoMaker) VerifyToken(token string) (*Payload, error) {}
OK, now the red line is gone. Let’s implement the CreateToken()
method.
Implement PASETO CreateToken method
Similar as before, we first have to create a new payload
with the input username
and duration
. If error is not nil
, we return an empty string and the error to the caller.
Otherwise, we return maker.paseto.Encrypt()
, and pass in the maker.symmetricKey
, and the payload
object. The last argument is an optional footer, which we don’t need, so I put nil
here.
func (maker *PasetoMaker) CreateToken(username string, duration time.Duration) (string, error) {
payload, err := NewPayload(username, duration)
if err != nil {
return "", err
}
return maker.paseto.Encrypt(maker.symmetricKey, payload, nil)
}
And that’s it! Pretty short and simple, right?
If we follow the implementation of this Encrypt()
function, we can see that, it is using Chacha Poly
cipher algorithm.
And inside the newCipher()
function, it also checks the input key size
to make sure that it equals to 32 bytes
.
Implement PASETO VerifyToken method
Alright, now let’s go back to our code and implement the VerifyToken()
method. It’s very simple!
We just need to declare an empty payload
object to store the decrypted data. Then call maker.paseto.Decrypt()
with the input token
, the symmetricKey
, the payload
and a nil
footer.
func (maker *PasetoMaker) VerifyToken(token string) (*Payload, error) {
payload := &Payload{}
err := maker.paseto.Decrypt(token, maker.symmetricKey, payload, nil)
if err != nil {
return nil, ErrInvalidToken
}
err = payload.Valid()
if err != nil {
return nil, err
}
return payload, nil
}
If error
is not nil
, we return nil
payload an ErrInvalidToken
. Else, we will check if the token is valid or not by calling payload.Valid()
.
If there’s an error, we just return nil
payload and the error
itself. Otherwise, we return the payload
and a nil
error.
And that’s it! Very concise and much simpler than JWT, right?
Test PASETO Maker
OK, now let’s write some unit tests!
I’m gonna create a new file: paseto_maker_test.go
inside the token
package. Actually the test would be almost identical to the one we wrote for JWT, so I’m just gonna copy it here.
Change its name to TestPasetoMaker
. Then here, instead of NewJWTMaker()
, we call NewPasetoMaker()
.
func TestPasetoMaker(t *testing.T) {
maker, err := NewPasetoMaker(util.RandomString(32))
require.NoError(t, err)
username := util.RandomOwner()
duration := time.Minute
issuedAt := time.Now()
expiredAt := issuedAt.Add(duration)
token, err := maker.CreateToken(username, duration)
require.NoError(t, err)
require.NotEmpty(t, token)
payload, err := maker.VerifyToken(token)
require.NoError(t, err)
require.NotEmpty(t, token)
require.NotZero(t, payload.ID)
require.Equal(t, username, payload.Username)
require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second)
require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second)
}
We don’t have to change anything else because PasetoMaker
implements the same token.Maker
interface as JWTMaker
.
Let’s run the test!
It passed!
Now let’s copy the test for the expired token case! Change its name to TestExpiredPasetoToken
, and update this call to NewPasetoMaker()
.
func TestExpiredPasetoToken(t *testing.T) {
maker, err := NewPasetoMaker(util.RandomString(32))
require.NoError(t, err)
token, err := maker.CreateToken(util.RandomOwner(), -time.Minute)
require.NoError(t, err)
require.NotEmpty(t, token)
payload, err := maker.VerifyToken(token)
require.Error(t, err)
require.EqualError(t, err, ErrExpiredToken.Error())
require.Nil(t, payload)
}
Then run the test!
It also passed. Excellent!
We don’t need the last test because the None
algorithm just doesn’t exist in PASETO. You can write another test to check the invalid token case if you want. I leave it as an exercise for you to practice.
And that brings us to the end of this lecture. We have learned how to implement both JWT and PASETO using Go to create and verify access tokens.
In the next article, I will show you how to use them in the login API, where users provide their username & password, and the server will return an access token if the provided credentials are correct.
Thanks a lot for reading, and see you soon in the next lecture!
If you like the article, please subscribe to our Youtube channel and follow us on Twitter or Facebook for more tutorials in the future.
If you want to join me on my current amazing team at Voodoo, check out our job openings here. Remote or onsite in Paris/Amsterdam/London/Berlin/Barcelona with visa sponsorship.
Top comments (3)
This is one of the most high quality backend series in GO, seriously under appreciated. Thanks for sharing this knowledge.
Hi, I'm really curious, maybe it's a stupid question. in the previous learning material with the title "how to write stronger unit tests..." you used go mock to make it easier to create unit tests. whereas for this material you don't use it. So when exactly to use Go Mock and when not?
Very well detailed, I appreciate that you also provide snippets.