When it comes to an API, we need authentication for users to access their information. JWT is widely used for API authentication because of its stateless nature. In this tutorial series, I will share with you what I’ve learned when I implemented JWT authentication.
There are three parts in this tutorial
- Part 1 — Public/secret key generation and storage
- Part 2 — Build a CLI to create/retrieve App object
- Part 3 — Build the JWT authentication middleware
For this tutorial, we’ll use the following tools:
- Golang 1.11 Darwin
- Postgres
- JWT library recommended by jwt.io: github.com/dgrijalva/jwt-go
- Golang built-in crypto packages
- Developer (very important: please feed some coffee fuel before use)
Assumptions:
- You know AES encryption (just enough to use, no need to read the research papers)
Disclaimer: All the codes here are not 100% mine. It’s a compilation from multiple sources such as open-source projects, go documentation, etc.
I will include the resources I used at the end of the tutorial. For now, let’s dive in.
⚙️ The process
To authenticate with our API, every request must include a public key and a JWT token in its headers. The JWT token is a bearer token in “Authorization” header. The process to authenticate a request is as follow:
- When our API receives the request, we check for its public key header.
- We use that public key to check if the app trying to access our API is registered in our database.
- If we found the app using its public key, we will use its secret key (also stored in our database) to verify its JWT signature.
- Otherwise, we return status 401 to the client.
The whole process can be visualized like this
🗝 The key couple
For any apps that want to use our API, we need to give them a secret and public key to authenticate. To do this, we will generate two random 16 bytes keys and store it in our database. To protect the secret keys, we will use AES encryption to encrypt the key before we write it to the database.
Why encryption? The secret key is an app’s password, it needs to be protected. However, unlike passwords, we will need the original value to verify the JWT signature. Therefore, encryption is a better solution to store the secret keys in our database.
There are two parts we need to use AES encryption. First, we need a master secret key on our side. We will use the same secret to encrypt all secret keys. Second, for each secret key, we will include a salt (or iv in this case) to vary the final encrypted key. The final data struct will look something like this:
// App is a generated public/secret key pair
type App struct {
Id int
Name string
EncryptedSecretKey string
PublicKey string
CreatedAt time.Time
UpdatedAt time.Time
}
Now we only need to write a SQL command to create an App table with those columns
Next, we will write a function to generate a random key string. This function will generate a 32 bytes random string to be used as a public key or secret key. Basically, we’ll call it twice to get the original values for both keys. You can change the number of bytes to adjust the length of the key. One thing to note is fmt.Sprintf("%x", key) will print out 2 characters per byte (documentation), so you will get a string twice the length of the byte array. In our case, it will generate a string of 64 characters. Read more about go strings here.
🔒 Encryption
To store our secret key securely in the database, we will have a master key used to encrypt every secret key with an Initial Value (IV, or nonce, or salt, whatever you’d like to call it). The master key will be stored as an environment variable (for simplicity). The length of the master key is important to determine whether the final encrypted value is an AES128, 192 or 256. The master key must be 32, 48 or 64 characters long, corresponding to AES128, 192, and 256.
In this tutorial, we will use AES256-GCM to encrypt our secret key. Go has a built-in aes package that can do the job for us. Here are our Encrypt and Decrypt functions.
First is our Encrypt function. I’ve separated it from our Decrypt function below so we have everything we need in one file. The process can be simplified as follow:
- Get the master secret key.
- Make a new AES cipher block.
- Make a new GCM cipher block which returns an AEAD object.
- Generate a random IV with the same size of the required nonce length (default to 12 bytes). This IV will be prepended to the final key.
- Finally, “Seal” the nonce and in the key to an encrypted value to a destination (dst). That’s why we use the nonce as the destination. It will append the encrypted key to the nonce. Here for reference.
Seal(dst, nonce, plaintext, additionalData []byte) []byte)
🔓 Decryption
Similar to our encryption process, we will utilize go’s built-in packages to decrypt encrypted keys stored in our database to be used in JWT signature verification. Here is our function.
Again, here’s the summary of our process.
- Get the master secret key.
- Make a new AES cipher block.
- Make a new GCM cipher block which returns an AEAD object.
- Verify the encrypted key’s length.
- Finally, “Open” the encrypted key by passing in nil for the destination, then the nonce which was prepended in the final key, then the actual encrypted key bytes (the latter part), and nil for extra data.
Open(dst, nonce, ciphertext, additionalData [][byte](https://golang.org/pkg/builtin/#byte)) ([][byte](https://golang.org/pkg/builtin/#byte), [error](https://golang.org/pkg/builtin/#error)
That’s it for our encryption functions. Here is the final code on Github. I hope this is helpful 😁.
Resources
- https://www.comparitech.com/blog/information-security/what-is-aes-encryption
- https://golang.org/pkg/crypto/cipher/
- https://en.wikipedia.org/wiki/Galois/Counter_Mode
- https://golang.org/pkg/crypto/aes/#NewCipher
- https://golang.org/pkg/encoding/hex/#DecodeString
- https://github.com/golang/go/blob/master/src/crypto/cipher/gcm.go#L162
- https://blog.tclaverie.eu/posts/understanding-golangs-aes-implementation-t-tables/
- https://github.com/gtank/cryptopasta.
Top comments (1)
Thanks for creating this series. I need to know this for a current project.