How to create and verify JWT & PASETO token in Golang

techschoolguru

TECH SCHOOL

Posted on April 5, 2021

How to create and verify JWT & PASETO token in Golang

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:

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)
}


Enter fullscreen mode Exit fullscreen mode

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"`
}


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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
}


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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
}


Enter fullscreen mode Exit fullscreen mode

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
}


Enter fullscreen mode Exit fullscreen mode

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.

Alt Text

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) {}


Enter fullscreen mode Exit fullscreen mode

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))
}


Enter fullscreen mode Exit fullscreen mode

Finally, to generate a token string, we call jwtToken.SignedString(), and pass in the secretKey after converting it to []byte slice.

Alt Text

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
}


Enter fullscreen mode Exit fullscreen mode

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
)


Enter fullscreen mode Exit fullscreen mode

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)
    ...
}


Enter fullscreen mode Exit fullscreen mode

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")
)


Enter fullscreen mode Exit fullscreen mode

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.

Alt Text

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
    }

    ...
}


Enter fullscreen mode Exit fullscreen mode

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
    }



Enter fullscreen mode Exit fullscreen mode

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 be time.Now()
  • And we add the duration to this issuedAt time to get the expiredAt 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)
}


Enter fullscreen mode Exit fullscreen mode

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 input username.
  • We use require.WithinDuration to compare the payload.IssuedAt field with the expected issuedAt time we saved above. They should not be different by more than 1 second.
  • Likewise, we compare the payload.ExpiredAt field with the expected expiredAt time in the same manner.

And we’re done! Let’s run this unit test!

Alt Text

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)
}


Enter fullscreen mode Exit fullscreen mode

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!

Alt Text

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)
}


Enter fullscreen mode Exit fullscreen mode

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!

Alt Text

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


Enter fullscreen mode Exit fullscreen mode

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
}


Enter fullscreen mode Exit fullscreen mode

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
}


Enter fullscreen mode Exit fullscreen mode

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) {}


Enter fullscreen mode Exit fullscreen mode

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)
}



Enter fullscreen mode Exit fullscreen mode

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.

Alt Text

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
}


Enter fullscreen mode Exit fullscreen mode

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)
}


Enter fullscreen mode Exit fullscreen mode

We don’t have to change anything else because PasetoMaker implements the same token.Maker interface as JWTMaker.

Let’s run the test!

Alt Text

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)
}


Enter fullscreen mode Exit fullscreen mode

Then run the test!

Alt Text

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.

💖 💪 🙅 🚩
techschoolguru
TECH SCHOOL

Posted on April 5, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related