This time, I'm going to make authentication system with JWT and Redis
I will use docker to run Redis in background
docker run -d -p 6379:6379 redis
Then create a Go module
mkdir jwt-redis
cd jwt-redis
go mod init gojwt
touch main.go
I need four libraries for this article
Before writing the server, I will prepare token functions First.
Create auth/token.go
file
package auth
import (
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt"
"github.com/google/uuid"
)
var (
_accessSecret = "Acc3ss"
_refreshSecret = "l23fr3sh"
)
type Token struct {
Token string
UUID string
Expire int64
}
func GenerateToken(username string, isRefresh bool) (Token, error) {
randomUUID := uuid.New().String()
claims := jwt.MapClaims{
"iss": "MY_APP",
"iat": time.Now().Unix(),
"username": username,
}
var (
ss string
expire int64
err error
secret string
)
if isRefresh {
expire = time.Now().Add(time.Minute * 10).Unix()
claims["refresh_uuid"] = randomUUID
secret = _refreshSecret
} else {
expire = time.Now().Add(time.Minute).Unix()
claims["access_uuid"] = randomUUID
claims["authorized"] = true
secret = _accessSecret
}
claims["exp"] = expire
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err = token.SignedString([]byte(secret))
return Token{
Token: ss,
UUID: randomUUID,
Expire: expire,
}, err
}
func GeneratePairToken(username string) ([]Token, error) {
tokens := make([]Token, 2)
var err error
if tokens[0], err = GenerateToken(username, false); err != nil {
return nil, err
}
if tokens[1], err = GenerateToken(username, true); err != nil {
return nil, err
}
return tokens, nil
}
func ExtractToken(token string, isRefresh bool) (jwt.MapClaims, error) {
secret := _accessSecret
if isRefresh {
secret = _refreshSecret
}
v, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected method (%v)", t.Header["alg"])
}
return []byte(secret), nil
})
if err != nil {
return nil, err
}
claims, ok := v.Claims.(jwt.MapClaims)
if !ok || !v.Valid {
return nil, errors.New("Can't parse")
}
return claims, nil
}
For this application, I will create 3 endpoints
Then my main.go
will be
package main
import (
"context"
"errors"
"fmt"
"gojwt/auth"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
)
func saveDataInRedis(rdb *redis.Client, key, value string, expire int64) error {
return rdb.Set(context.Background(), key, value, time.Unix(expire, 0).Sub(time.Now())).Err()
}
func AuthMiddleware(rdb *redis.Client) gin.HandlerFunc {
return func(ctx *gin.Context) {
splits := strings.Split(ctx.Request.Header.Get("Authorization"), " ")
if len(splits) != 2 {
ctx.AbortWithError(http.StatusUnauthorized, errors.New("No auth token"))
return
}
token := splits[1]
claims, err := auth.ExtractToken(token, false)
if err != nil {
ctx.AbortWithError(http.StatusUnauthorized, errors.New("Invalid token"))
return
}
uuid, ok := claims["access_uuid"].(string)
if !ok {
ctx.AbortWithError(http.StatusUnauthorized, errors.New("Token info is missing"))
return
}
username, err := rdb.Get(context.Background(), uuid).Result()
if err != nil {
ctx.AbortWithError(http.StatusUnauthorized, errors.New("Token is expired"))
return
}
ctx.Set("accessuuid", uuid)
ctx.Set("username", username)
}
}
type (
LoginForm struct {
Username string
}
RefreshRequestForm struct {
RefreshToken string
}
LogoutRequestForm struct {
RefreshToken string
}
)
func main() {
r := gin.Default()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
r.POST("/login", func(ctx *gin.Context) {
var form LoginForm
if err := ctx.ShouldBindJSON(&form); err != nil {
ctx.String(http.StatusUnprocessableEntity, "Wrong format")
return
}
tokens, err := auth.GeneratePairToken(form.Username)
if err != nil {
ctx.String(http.StatusInternalServerError, "Something went wrong")
return
}
if err := saveDataInRedis(rdb, tokens[0].UUID, form.Username, tokens[0].Expire); err != nil {
ctx.String(http.StatusInternalServerError, "Something went wrong")
return
}
if err := saveDataInRedis(rdb, tokens[1].UUID, form.Username, tokens[1].Expire); err != nil {
ctx.String(http.StatusInternalServerError, "Something went wrong")
return
}
ctx.JSON(http.StatusOK, gin.H{
"AccessToken": tokens[0].Token,
"RefreshToken": tokens[1].Token,
})
})
authRoute := r.Group("/", AuthMiddleware(rdb))
authRoute.GET("/resource", func(ctx *gin.Context) {
var username string
if v, ok := ctx.Get("username"); !ok {
ctx.String(http.StatusForbidden, "Something went wrong")
return
} else if username, ok = v.(string); !ok {
ctx.String(http.StatusForbidden, "Some info is missing")
return
}
ctx.String(http.StatusOK, fmt.Sprintf("Hello %s!", username))
})
r.POST("/refresh", func(ctx *gin.Context) {
var form RefreshRequestForm
if err := ctx.ShouldBindJSON(&form); err != nil {
ctx.String(http.StatusUnprocessableEntity, "Wrong format")
return
}
claims, err := auth.ExtractToken(form.RefreshToken, true)
if err != nil {
ctx.AbortWithError(http.StatusUnauthorized, errors.New("Invalid token"))
return
}
username, ok := claims["username"].(string)
if !ok {
ctx.AbortWithError(http.StatusUnauthorized, errors.New("Token info is missing"))
return
}
accessToken, err := auth.GenerateToken(username, false)
if err != nil {
ctx.String(http.StatusInternalServerError, "Something went wrong")
return
}
if err := saveDataInRedis(rdb, accessToken.UUID, username, accessToken.Expire); err != nil {
ctx.String(http.StatusInternalServerError, "Something went wrong")
return
}
ctx.JSON(http.StatusOK, gin.H{
"AccessToken": accessToken.Token,
})
})
authRoute.POST("/logout", func(ctx *gin.Context) {
var form LogoutRequestForm
if err := ctx.ShouldBindJSON(&form); err != nil {
ctx.String(http.StatusUnprocessableEntity, "Wrong format")
return
}
var accessuuid string
if v, ok := ctx.Get("accessuuid"); !ok {
ctx.String(http.StatusForbidden, "Something went wrong")
return
} else if accessuuid, ok = v.(string); !ok {
ctx.String(http.StatusForbidden, "Some info is missing")
return
}
refreshClaims, err := auth.ExtractToken(form.RefreshToken, true)
if err != nil {
ctx.AbortWithError(http.StatusUnauthorized, errors.New("Invalid token"))
return
}
refreshuuid, ok := refreshClaims["refresh_uuid"].(string)
if !ok {
ctx.AbortWithError(http.StatusUnauthorized, errors.New("Token info is missing"))
return
}
rdb.Del(context.Background(), accessuuid)
rdb.Del(context.Background(), refreshuuid)
ctx.String(http.StatusOK, "Logged out successfully")
})
r.Run()
}
Steps:
/login
endpoint. This end point requires a username to be sent via request body/resource
endpoint, need to add the access token in request header. The /resource
endpoint will extract the token and response a greeting./refresh
endpoint to get a new access token. This endpoint requires a refresh token in request body/logout
endpoint with the access token in request header and the refresh token in request bodyP.S. An access token will be valid for a minute after the server generated the token and for a refresh token it will be 10 minutes
sources