Gerasimos (Makis) Maropoulos
Posted on July 9, 2023
In this tutorial, we will learn how to use JWT (JSON Web Token) authentication with Iris, a fast and simple web framework for Go. JWT is a standard for securely transmitting information between parties as a JSON object. It can be used to authenticate users and protect API endpoints from unauthorized access.
Prerequisites
To follow this tutorial, you will need:
- Go 1.20 or higher installed on your machine
- A basic understanding of Go and Iris
- A text editor or IDE of your choice (e.g. VS Code)
Creating a new Iris project
First, we will create a new Iris project using the go mod
command. To do that, run the following commands:
$ mkdir iris-jwt
$ cd iris-jwt
$ go mod init iris-jwt
$ go get github.com/kataras/iris/v12@latest
This will create a new folder called iris-jwt
with the following files:
iris-jwt
├── go.mod
└── go.sum
The go.mod
file contains the module name and the dependency on Iris. The go.sum
file contains the checksums of the dependencies.
Next, we will create a file called main.go
in the same folder and write some basic code to start an Iris server on port 8080. We will modify this file later to add our JWT logic.
package main
import "github.com/kataras/iris/v12"
func main() {
// Create a new Iris application.
app := iris.New()
// Register a simple GET handler at the root path.
app.Get("/", func(ctx iris.Context) {
ctx.WriteString("Hello, world!")
})
// Start the server at http://localhost:8080.
app.Listen(":8080")
}
To run the server, run the following command:
$ go run main.go
You should see something like this in your terminal:
Now listening on: http://localhost:8080
Application started. Press CTRL+C to shut down.
You can also visit http://localhost:8080 in your browser and see the message "Hello, world!".
Defining the user model and the JWT claims
Now, we will define a simple user model and a custom JWT claims struct in our main.go
file. The user model will represent the data of our users, such as username and password. The JWT claims struct will contain the information that we want to store in our JWT tokens, such as user ID and expiration time.
We will also define some constants for our secret keys and token expiration durations.
package main
import (
"time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt"
)
// User is a simple user model.
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
// Claims is a custom JWT claims struct.
type Claims struct {
jwt.Claims // embeds standard claims iat, exp and sub.
UserID int64 `json:"user_id"`
}
// Define some constants for our secret keys and token expiration durations.
const (
accessSecret = "my-access-secret"
refreshSecret = "my-refresh-secret"
accessExpire = 15 * time.Minute
refreshExpire = 24 * time.Hour
)
Creating a signer and a verifier for JWT
Next, we will create a signer and a verifier for JWT using the Iris middleware/jwt package. The signer will be used to generate and sign JWT tokens with our secret keys and claims. The verifier will be used to verify and validate JWT tokens from the client requests.
We will create two signers and one verifiers, one pair for the access token and one signer for the refresh token. We will also use encryption for our tokens to add an extra layer of security.
package main
import (
"time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt"
)
// ...
// Create a signer for the access token.
accessSigner := jwt.NewSigner(jwt.HS256, accessSecret, accessExpire).
// Use encryption for the access token.
WithEncryption([]byte("my-access-encryption-key"), nil)
// Create a signer for the refresh token.
refreshSigner := jwt.NewSigner(jwt.HS256, refreshSecret, refreshExpire).
// Use encryption for the refresh token.
WithEncryption([]byte("my-refresh-encryption-key"), nil)
// Create a verifier for the access token.
accessVerifier := jwt.NewVerifier(jwt.HS256, accessSecret).
// Use decryption for the access token.
WithDecryption([]byte("my-access-encryption-key"), nil)
// Use a blocklist to revoke tokens.
// .WithDefaultBlocklist()
Creating some mock users and a login handler
For the sake of simplicity, we will create some mock users in a map and use them to simulate authentication. In a real application, you would use a database or another storage system to store and retrieve your users.
We will also create a login handler that will take a username and password from the client request, check if they match with one of our mock users, and if so, generate an access token and a refresh token for that user and send them back to the client.
package main
import (
"time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt"
)
// ...
// Create some mock users in a map.
users := map[string]User{
"alice": {ID: 1, Username: "alice", Password: "1234"},
"bob": {ID: 2, Username: "bob", Password: "5678"},
}
// Create a login handler that will generate JWT tokens for authenticated users.
loginHandler := func(ctx iris.Context) {
// Get the username and password from the request body.
var user User
if err := ctx.ReadJSON(&user); err != nil {
ctx.StopWithError(iris.StatusBadRequest, err)
return
}
// Check if the username and password match with one of our mock users.
if u, ok := users[user.Username]; !ok || u.Password != user.Password {
ctx.StopWithStatus(iris.StatusUnauthorized)
return
}
// Generate an access token with the user ID as the subject claim.
accessClaims := Claims{
Claims: jwt.Claims{Subject: u.Username},
UserID: u.ID,
}
accessToken, err := accessSigner.Sign(accessClaims)
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
// Generate a refresh token with the user ID as the subject claim.
refreshClaims := Claims{
Claims: jwt.Claims{Subject: u.Username},
UserID: u.ID,
}
refreshToken, err := refreshSigner.Sign(refreshClaims)
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
/* OR
tokenPair, err := refreshSigner.NewTokenPair(accessClaims, refreshClaims, refreshExpire)
// [handle err...]
ctx.JSON(tokenPair)
*/
// Send the tokens to the client as JSON.
ctx.JSON(iris.Map{
"access_token": string(accessToken),
"refresh_token": string(refreshToken),
"expires_in": int64(accessExpire.Seconds()),
})
}
Creating a protected API handler
Next, we will create a protected API handler that will only allow authorized users to access it. We will use the access verifier as a middleware to verify and validate the access token from the client request. If the token is valid, we will extract the user ID from it and send it back to the client as JSON.
package main
import (
"time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt"
)
// ...
// Create a protected API handler that will only allow authorized users to access it.
protectedHandler := func(ctx iris.Context) {
// Get the verified token from the context.
token := jwt.GetVerifiedToken(ctx) // important step.
// Get the custom claims from the token.
var claims Claims
if err := token.Claims(&claims); err != nil { // important step.
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
// Get the user ID from the claims.
userID := claims.UserID
// Send the user ID to the client as JSON.
// This is just an example, you can do whatever you want here.
ctx.JSON(iris.Map{
"user_id": userID,
})
}
Creating a refresh handler
Finally, we will create a refresh handler that will allow users to refresh their access tokens using their refresh tokens. We will use the refresh verifier as a middleware to verify and validate the refresh token from the client request. If the token is valid, we will generate a new access token and a new refresh token for the same user and send them back to the client.
package main
import (
"time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt"
)
// ...
// Create a refresh handler that will allow users to refresh their access tokens using their refresh tokens.
refreshHandler := func(ctx iris.Context) {
// Get the verified token from the context.
token := jwt.GetVerifiedToken(ctx)
// Get the custom claims from the token.
var claims Claims
if err := token.Claims(&claims); err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
// Get the user ID and username from the claims.
userID := claims.UserID
username := claims.Subject
// Generate a new access token with the same user ID and username as the subject claim.
accessClaims := Claims{
Claims: jwt.Claims{Subject: username},
UserID: userID,
}
accessToken, err := accessSigner.Sign(accessClaims)
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
// Generate a new refresh token with the same user ID and username as the subject claim.
refreshClaims := Claims{
Claims: jwt.Claims{Subject: username},
UserID: userID,
}
refreshToken, err := refreshSigner.Sign(refreshClaims)
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
// Send the new tokens to the client as JSON.
ctx.JSON(iris.Map{
"access_token": string(accessToken),
"refresh_token": string(refreshToken),
"expires_in": int64(accessExpire.Seconds()),
})
}
Registering the handlers and testing the application
Now that we have created all our handlers, we can register them with our Iris application and test our application. We will use the app.Post
method to register our login, protected, and refresh handlers with different paths. We will also use the accessVerifier.Verify
method to register our verifiers as middlewares for the rest of the protected handlers.
package main
import (
"time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt"
)
// ...
func main() {
// Create a new Iris application.
app := iris.New()
// Register our login handler at /login path.
app.Post("/login", loginHandler)
// Register a single protected handler at /protected path.
// This handler verifies the request manually as we've seen above.
app.Post("/protected", protectedHandler)
// Register our refresh handler at /refresh path with refresh signer.
app.Post("/refresh", refreshHandler)
// You can also register the pre-defined jwt middleware for all protected routes
// which performs verification automatically and set the custom Claims to the Context
// for next handlers to use through: claims := jwt.Get(ctx).(*Claims).
app.Use(accessVerifier.Verify(func() interface{} {
return new(Claims)
}))
// [more routes...]
// Start the server at http://localhost:8080.
app.Listen(":8080")
}
To test our application, we can use a tool like curl or Postman to send HTTP requests to our server. Here are some examples of how to do that:
- To login as Alice and get the access token and the refresh token, we can send a POST request to http://localhost:8080/login with the following JSON body:
{
"username": "alice",
"password": "1234"
}
We should get a response like this:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYwNjQ3MzEsImlhdCI6MTYzNjA2MzIzMSwic3ViIjoiYWxpY2UiLCJ1c2VySWQiOjF9.8f7a7b8f7a7b8f7a7b8f7a7b8f7a7b8f7a7b8f7a7b8f7a7b8f7a7b8f",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYxNTAzMzEsImlhdCI6MTYzNjA2MzIzMSwic3ViIjoiYWxpY2UiLCJ1c2VySWQiOjF9.9f8a8b9f8a8b9f8a8b9f8a8b9f8a8b9f8a8b9f8a8b9f8a8b9f8a8b9f",
"expires_in": 900
}
- To access the protected API endpoint with the access token, we can send a POST request to http://localhost:8080/protected with the following header:
Authorization: Bearer <access-token>
We should get a response like this:
{
"user_id": 1
}
- To refresh the access token with the refresh token, we can send a POST request to http://localhost:8080/refresh with the following header:
Authorization: Bearer <refresh-token>
We should get a response like this:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYwNjQ4NTMsImlhdCI6MTYzNjA2MzM1Mywic3ViIjoiYWxpY2UiLCJ1c2VySWQiOjF9.9g7a7c9g7a7c9g7a7c9g7a7c9g7a7c9g7a7c9g7a7c9g7a7c9g7a7c9g",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYxNTA0NTMsImlhdCI6MTYzNjA2MzM1Mywic3ViIjoiYWxpY2UiLCJ1c2VySWQiOjF9.aG8aBbaG8aBbaG8aBbaG8aBbaG8aBbaG8aBbaG8aBbaG8aBbaG8aBbaG",
"expires_in": 900
}
Conclusion
In this tutorial, we learned how to use JWT authentication with Iris, a fast and simple web framework for Go. We learned how to create a signer and a verifier for JWT, how to generate and validate JWT tokens, how to protect an API endpoint with JWT, and how to refresh JWT tokens. We also learned how to use the Iris middleware/jwt package, which provides a common Iris handler for JWT authentication.
You can find the complete code for this tutorial on GitHub: https://github.com/kataras/iris/tree/master/_examples/auth/jwt/tutorial and https://github.com/kataras/jwt.
If you want to learn more about Iris, you can visit its official website: https://www.iris-go.com/
If you want to learn more about JWT, you can visit its official website: https://jwt.io/
I hope you enjoyed this tutorial and found it useful. Thank you for reading.😊
Posted on July 9, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.