19
loading...
This website collects cookies to deliver better user experience
go get github.com/cristalhq/jwt/v3
.export RGB_JWT_SECRET=jwtSecret123
to our .env
file. Of course, in production you would want to use some long randomly generated string.internal/conf/conf.go
. We will add constant jwtSecretKey = "RGB_JWT_SECRET"
with the rest of our constants and then add new field JwtSecret
of type string to Config
struct. Now we can read new env variable and add it inside of NewConfig()
function:const (
hostKey = "RGB_HOST"
portKey = "RGB_PORT"
dbHostKey = "RGB_DB_HOST"
dbPortKey = "RGB_DB_PORT"
dbNameKey = "RGB_DB_NAME"
dbUserKey = "RGB_DB_USER"
dbPasswordKey = "RGB_DB_PASSWORD"
jwtSecretKey = "RGB_JWT_SECRET"
)
type Config struct {
Host string
Port string
DbHost string
DbPort string
DbName string
DbUser string
DbPassword string
JwtSecret string
}
func NewConfig() Config {
host, ok := os.LookupEnv(hostKey)
if !ok || host == "" {
logAndPanic(hostKey)
}
port, ok := os.LookupEnv(portKey)
if !ok || port == "" {
if _, err := strconv.Atoi(port); err != nil {
logAndPanic(portKey)
}
}
dbHost, ok := os.LookupEnv(dbHostKey)
if !ok || dbHost == "" {
logAndPanic(dbHostKey)
}
dbPort, ok := os.LookupEnv(dbPortKey)
if !ok || dbPort == "" {
if _, err := strconv.Atoi(dbPort); err != nil {
logAndPanic(dbPortKey)
}
}
dbName, ok := os.LookupEnv(dbNameKey)
if !ok || dbName == "" {
logAndPanic(dbNameKey)
}
dbUser, ok := os.LookupEnv(dbUserKey)
if !ok || dbUser == "" {
logAndPanic(dbUserKey)
}
dbPassword, ok := os.LookupEnv(dbPasswordKey)
if !ok || dbPassword == "" {
logAndPanic(dbPasswordKey)
}
jwtSecret, ok := os.LookupEnv(jwtSecretKey)
if !ok || jwtSecret == "" {
logAndPanic(jwtSecretKey)
}
return Config{
Host: host,
Port: port,
DbHost: dbHost,
DbPort: dbPort,
DbName: dbName,
DbUser: dbUser,
DbPassword: dbPassword,
JwtSecret: jwtSecret,
}
}
internal/server/jwt.go
:package server
import (
"rgb/internal/conf"
"github.com/cristalhq/jwt/v3"
"github.com/rs/zerolog/log"
)
var (
jwtSigner jwt.Signer
jwtVerifier jwt.Verifier
)
func jwtSetup(conf conf.Config) {
var err error
key := []byte(conf.JwtSecret)
jwtSigner, err = jwt.NewSignerHS(jwt.HS256, key)
if err != nil {
log.Panic().Err(err).Msg("Error creating JWT signer")
}
jwtVerifier, err = jwt.NewVerifierHS(jwt.HS256, key)
if err != nil {
log.Panic().Err(err).Msg("Error creating JWT verifier")
}
}
jwtSetup()
will only create signer and verifier that will later be used in authentication. Now we can call this function from internal/server/server/go
when starting server:package server
import (
"rgb/internal/conf"
"rgb/internal/database"
"rgb/internal/store"
)
func Start(cfg conf.Config) {
jwtSetup(cfg)
store.SetDBConnection(database.NewDBOptions(cfg))
router := setRouter()
// Start listening and serving requests
router.Run(":8080")
}
internal/server/jwt.go
:func generateJWT(user *store.User) string {
claims := &jwt.RegisteredClaims{
ID: fmt.Sprint(user.ID),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 7)),
}
builder := jwt.NewBuilder(jwtSigner)
token, err := builder.Build(claims)
if err != nil {
log.Panic().Err(err).Msg("Error building JWT")
}
return token.String()
}
internal/server/user.go
instead of hardcoded string we had so far for testing purposes:package server
import (
"net/http"
"rgb/internal/store"
"github.com/gin-gonic/gin"
)
func signUp(ctx *gin.Context) {
user := new(store.User)
if err := ctx.Bind(user); err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := store.AddUser(user); err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{
"msg": "Signed up successfully.",
"jwt": generateJWT(user),
})
}
func signIn(ctx *gin.Context) {
user := new(store.User)
if err := ctx.Bind(user); err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := store.Authenticate(user.Username, user.Password)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Sign in failed."})
return
}
ctx.JSON(http.StatusOK, gin.H{
"msg": "Signed in successfully.",
"jwt": generateJWT(user),
})
}
verifyJWT()
function in internal/server/jwt.go
. This function will receive token in the form of string, verify its signature, extract ID from claims and if everything is ok, user's ID will be returned as int:func verifyJWT(tokenStr string) (int, error) {
token, err := jwt.Parse([]byte(tokenStr))
if err != nil {
log.Error().Err(err).Str("tokenStr", tokenStr).Msg("Error parsing JWT")
return 0, err
}
if err := jwtVerifier.Verify(token.Payload(), token.Signature()); err != nil {
log.Error().Err(err).Msg("Error verifying token")
return 0, err
}
var claims jwt.StandardClaims
if err := json.Unmarshal(token.RawClaims(), &claims); err != nil {
log.Error().Err(err).Msg("Error unmarshalling JWT claims")
return 0, err
}
if notExpired := claims.IsValidAt(time.Now()); !notExpired {
return 0, errors.New("Token expired.")
}
id, err := strconv.Atoi(claims.ID)
if err != nil {
log.Error().Err(err).Str("claims.ID", claims.ID).Msg("Error converting claims ID to number")
return 0, errors.New("ID in token is not valid")
}
return id, err
}
internal/store/users.go
, add function:func FetchUser(id int) (*User, error) {
user := new(User)
user.ID = id
err := db.Model(user).Returning("*").WherePK().Select()
if err != nil {
log.Error().Err(err).Msg("Error fetching user")
return nil, err
}
return user, nil
}
internal/server/middleware.go
:package server
import (
"net/http"
"rgb/internal/store"
"strings"
"github.com/gin-gonic/gin"
)
func authorization(ctx *gin.Context) {
authHeader := ctx.GetHeader("Authorization")
if authHeader == "" {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing."})
return
}
headerParts := strings.Split(authHeader, " ")
if len(headerParts) != 2 {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format is not valid."})
return
}
if headerParts[0] != "Bearer" {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is missing bearer part."})
return
}
userID, err := verifyJWT(headerParts[1])
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
user, err := store.FetchUser(userID)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
ctx.Set("user", user)
ctx.Next()
}
Authorization
header. It first checks if header exists, if it's in valid format, and then calls verifyJWT()
function. If JWT verification passes, user's ID is returned. User with that ID is fetched from database and set as current user for this context.func currentUser(ctx *gin.Context) (*store.User, error) {
var err error
_user, exists := ctx.Get("user")
if !exists {
err = errors.New("Current context user not set")
log.Error().Err(err).Msg("")
return nil, err
}
user, ok := _user.(*store.User)
if !ok {
err = errors.New("Context user is not valid type")
log.Error().Err(err).Msg("")
return nil, err
}
return user, nil
}
ctx.Get()
returns interface, we must check if value is of type *store.User
. If not, error is returned. When both checks are passed, current user is returned from context.