impl service layer for auth
This commit is contained in:
parent
63b443171d
commit
e8f0e43836
|
|
@ -4,7 +4,7 @@ CREATE TABLE users (
|
|||
last_name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
phone_number VARCHAR(20) UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
password BYTEA NOT NULL,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP ,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ type User struct {
|
|||
LastName string
|
||||
Email string
|
||||
PhoneNumber string
|
||||
Password string
|
||||
Password []byte
|
||||
Role string
|
||||
Verified pgtype.Bool
|
||||
CreatedAt pgtype.Timestamp
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ type CreateUserParams struct {
|
|||
LastName string
|
||||
Email string
|
||||
PhoneNumber string
|
||||
Password string
|
||||
Password []byte
|
||||
Role string
|
||||
Verified pgtype.Bool
|
||||
}
|
||||
|
|
@ -129,7 +129,7 @@ type UpdateUserParams struct {
|
|||
LastName string
|
||||
Email string
|
||||
PhoneNumber string
|
||||
Password string
|
||||
Password []byte
|
||||
Role string
|
||||
Verified pgtype.Bool
|
||||
}
|
||||
|
|
|
|||
10
go.mod
10
go.mod
|
|
@ -5,32 +5,38 @@ go 1.24.1
|
|||
require (
|
||||
github.com/bytedance/sonic v1.13.2
|
||||
github.com/gofiber/fiber/v2 v2.52.6
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/jackc/pgx/v5 v5.7.4
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/crypto v0.32.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
11
go.sum
11
go.sum
|
|
@ -8,11 +8,14 @@ github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFos
|
|||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
|
|
@ -30,6 +33,10 @@ github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ib
|
|||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
|
|
@ -41,6 +48,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
|
@ -72,6 +81,8 @@ golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -1 +1,12 @@
|
|||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type RefreshToken struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
Token string
|
||||
ExpiresAt time.Time
|
||||
CreatedAt time.Time
|
||||
Revoked bool
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
ID int64
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
PhoneNumber string
|
||||
Password string
|
||||
Password []byte
|
||||
Role string
|
||||
Verified bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ func (s *Store) CreateUser(ctx context.Context, firstName, lastName, email, phon
|
|||
LastName: lastName,
|
||||
Email: email,
|
||||
PhoneNumber: phoneNumber,
|
||||
Password: password,
|
||||
// Password: password,
|
||||
Role: role,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -71,7 +71,7 @@ func (s *Store) UpdateUser(ctx context.Context, id int64, firstName, lastName, e
|
|||
LastName: lastName,
|
||||
Email: email,
|
||||
PhoneNumber: phoneNumber,
|
||||
Password: password,
|
||||
// Password: password,
|
||||
Role: role,
|
||||
})
|
||||
return err
|
||||
|
|
|
|||
131
internal/services/authentication/impl.go
Normal file
131
internal/services/authentication/impl.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidPassword = errors.New("incorrect password")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrExpiredToken = errors.New("token expired")
|
||||
ErrRefreshTokenNotFound = errors.New("refresh token not found") // i.e login again
|
||||
)
|
||||
|
||||
func (s *Service) Login(ctx context.Context, emailPhone EmailPhone, password string) (string, error) {
|
||||
user, err := s.userStore.GetUserByEmailPhone(ctx, emailPhone)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = matchPassword(password, user.Password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// //create session
|
||||
// accessToken, err := CreateJwt(strconv.Itoa(int(user.ID)), s.jwtConfig.JwtAccessKey, s.jwtConfig.JwtAccessExpiry)
|
||||
// if err != nil {
|
||||
// return Tokens{}, err
|
||||
// }
|
||||
refreshToken, err := generateRefreshToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
|
||||
Token: refreshToken,
|
||||
UserID: user.ID,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return refreshToken, nil
|
||||
}
|
||||
|
||||
func (s *Service) RefreshToken(ctx context.Context, refToken string) (string, error) {
|
||||
// us, err := ParseJwt(tokens.RefreshToken, s.jwtConfig.JwtAccessKey)
|
||||
// if err == nil {
|
||||
// return Tokens{}, err
|
||||
// }
|
||||
// if !errors.Is(err, ErrExpiredToken) {
|
||||
// return Tokens{}, err
|
||||
// }
|
||||
token, err := s.tokenStore.GetRefreshToken(ctx, refToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if token.Revoked {
|
||||
return "", ErrRefreshTokenNotFound
|
||||
}
|
||||
if token.ExpiresAt.Before(time.Now()) {
|
||||
return "", ErrExpiredToken
|
||||
}
|
||||
//
|
||||
// naccessToken, err := CreateJwt(token., s.jwtConfig.JwtAccessKey, s.jwtConfig.JwtAccessExpiry)
|
||||
// if err != nil {
|
||||
// return Tokens{}, err
|
||||
// }
|
||||
newRefToken, err := generateRefreshToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// ntokens := Tokens{
|
||||
// AccessToken: naccessToken,
|
||||
// RefreshToken: nrefreshToken,
|
||||
// }
|
||||
|
||||
err = s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
|
||||
Token: newRefToken,
|
||||
UserID: token.UserID,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return newRefToken, nil
|
||||
}
|
||||
func (s *Service) Logout(ctx context.Context, refToken string) error {
|
||||
token, err := s.tokenStore.GetRefreshToken(ctx, refToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if token.Revoked {
|
||||
return ErrRefreshTokenNotFound
|
||||
}
|
||||
if token.ExpiresAt.Before(time.Now()) {
|
||||
return ErrExpiredToken
|
||||
}
|
||||
|
||||
return s.tokenStore.RevokeRefreshToken(ctx, refToken)
|
||||
}
|
||||
|
||||
func matchPassword(plaintextPassword string, hash []byte) error {
|
||||
err := bcrypt.CompareHashAndPassword(hash, []byte(plaintextPassword))
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword):
|
||||
return ErrInvalidPassword
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
func generateRefreshToken() (string, error) {
|
||||
randomBytes := make([]byte, 32)
|
||||
_, err := rand.Read(randomBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
plaintext := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randomBytes)
|
||||
return plaintext, nil
|
||||
}
|
||||
16
internal/services/authentication/port.go
Normal file
16
internal/services/authentication/port.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
)
|
||||
|
||||
type UserStore interface {
|
||||
GetUserByEmailPhone(ctx context.Context, emailPhone EmailPhone) (domain.User, error)
|
||||
}
|
||||
type TokenStore interface {
|
||||
CreateRefreshToken(ctx context.Context, rt domain.RefreshToken) error
|
||||
GetRefreshToken(ctx context.Context, token string) (domain.RefreshToken, error)
|
||||
RevokeRefreshToken(ctx context.Context, token string) error
|
||||
}
|
||||
28
internal/services/authentication/service.go
Normal file
28
internal/services/authentication/service.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package authentication
|
||||
|
||||
type EmailPhone struct {
|
||||
Email ValidString
|
||||
PhoneNumber ValidString
|
||||
Password ValidString
|
||||
}
|
||||
type ValidString struct {
|
||||
Value string
|
||||
Valid bool
|
||||
}
|
||||
type Tokens struct {
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
}
|
||||
type Service struct {
|
||||
userStore UserStore
|
||||
tokenStore TokenStore
|
||||
RefreshExpiry int
|
||||
}
|
||||
|
||||
func NewService(userStore UserStore, tokenStore TokenStore, RefreshExpiry int) *Service {
|
||||
return &Service{
|
||||
userStore: userStore,
|
||||
tokenStore: tokenStore,
|
||||
RefreshExpiry: RefreshExpiry,
|
||||
}
|
||||
}
|
||||
|
|
@ -12,4 +12,6 @@ type UserStore interface {
|
|||
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||
UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error
|
||||
DeleteUser(ctx context.Context, id int64) error
|
||||
//
|
||||
//GetUserByEmailPhone(ctx context.Context, emailPhone EmailPhone) (domain.User, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
userStore UserStore
|
||||
}
|
||||
|
||||
func NewService(userStore UserStore) *Service {
|
||||
func NewService(userStore UserStore, RefreshExpiry int) *Service {
|
||||
return &Service{
|
||||
userStore: userStore,
|
||||
}
|
||||
|
|
@ -31,3 +32,11 @@ func (s *Service) UpdateUser(ctx context.Context, id int64, firstName, lastName,
|
|||
func (s *Service) DeleteUser(ctx context.Context, id int64) error {
|
||||
return s.userStore.DeleteUser(ctx, id)
|
||||
}
|
||||
func hashPassword(plaintextPassword string) ([]byte, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
|
|
|
|||
53
internal/web_server/jwt/jwt.go
Normal file
53
internal/web_server/jwt/jwt.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// type UserToken struct {
|
||||
// UserId string
|
||||
// }
|
||||
var (
|
||||
ErrExpiredToken = errors.New("token expired")
|
||||
ErrMalformedToken = errors.New("token malformed")
|
||||
ErrTokenNotExpired = errors.New("token not expired")
|
||||
ErrRefreshTokenNotFound = errors.New("refresh token not found") // i.e login again
|
||||
)
|
||||
|
||||
type UserClaim struct {
|
||||
jwt.RegisteredClaims
|
||||
UserId string
|
||||
}
|
||||
|
||||
func CreateJwt(userId string, key string, expiry int) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim{RegisteredClaims: jwt.RegisteredClaims{Issuer: "github.com/lafetz/snippitstash",
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Audience: jwt.ClaimStrings{"fortune.com"},
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expiry) * time.Second))},
|
||||
UserId: userId,
|
||||
})
|
||||
jwtToken, err := token.SignedString([]byte(key)) //
|
||||
return jwtToken, err
|
||||
}
|
||||
func ParseJwt(jwtToken string, key string) (*UserClaim, error) {
|
||||
token, err := jwt.ParseWithClaims(jwtToken, &UserClaim{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(key), nil
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||
return nil, ErrExpiredToken
|
||||
}
|
||||
if errors.Is(err, jwt.ErrTokenMalformed) {
|
||||
return nil, ErrMalformedToken
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if claims, ok := token.Claims.(*UserClaim); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
89
internal/web_server/jwt/jwt_test.go
Normal file
89
internal/web_server/jwt/jwt_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package user
|
||||
|
||||
// func TestCreateJwt(t *testing.T) {
|
||||
// // Define a user to test
|
||||
// user := &domain.User{
|
||||
// ID: 123,
|
||||
// }
|
||||
|
||||
// // Secret key used for signing the JWT
|
||||
// secretKey := "secret"
|
||||
|
||||
// // Token expiry time (in seconds)
|
||||
// expiry := 3600 // 1 hour
|
||||
|
||||
// // Call CreateJwt function
|
||||
// tokenString, err := CreateJwt(user, secretKey, expiry)
|
||||
|
||||
// // Assertions
|
||||
// assert.NoError(t, err, "Error should be nil when creating a JWT")
|
||||
// assert.NotEmpty(t, tokenString, "Token string should not be empty")
|
||||
|
||||
// // Parse the token back and verify its claims
|
||||
// claims, err := ParseJwt(tokenString, secretKey)
|
||||
// assert.NoError(t, err, "Error should be nil when parsing the JWT")
|
||||
// assert.Equal(t, strconv.Itoa(int(user.ID)), claims.UserId, "User ID should match")
|
||||
// assert.Equal(t, "github.com/lafetz/snippitstash", claims.Issuer, "Issuer should match")
|
||||
// assert.True(t, claims.ExpiresAt.Time.After(time.Now()), "Token should not be expired yet")
|
||||
// expectedExpiryTime := time.Now().Add(time.Duration(expiry) * time.Second)
|
||||
// // Allow for a small margin of error due to the time delay in generating the token
|
||||
// assert.True(t, claims.ExpiresAt.Time.Before(expectedExpiryTime.Add(1*time.Second)),
|
||||
// "Token expiry time should be within the expected range")
|
||||
// assert.True(t, claims.ExpiresAt.Time.After(expectedExpiryTime.Add(-1*time.Second)),
|
||||
// "Token expiry time should be within the expected range")
|
||||
// }
|
||||
// func TestParseJwt(t *testing.T) {
|
||||
// // Define a user to test
|
||||
// user := &domain.User{
|
||||
// ID: 123,
|
||||
// }
|
||||
|
||||
// // Secret key used for signing the JWT
|
||||
// secretKey := "secret"
|
||||
|
||||
// // Token expiry time (in seconds)
|
||||
// expiry := 3600 // 1 hour
|
||||
|
||||
// // Generate a token using the CreateJwt function
|
||||
// tokenString, err := CreateJwt(user, secretKey, expiry)
|
||||
// assert.NoError(t, err, "Error should be nil when creating a JWT")
|
||||
// assert.NotEmpty(t, tokenString, "Token string should not be empty")
|
||||
|
||||
// // Now, we will parse the token
|
||||
// claims, err := ParseJwt(tokenString, secretKey)
|
||||
// assert.NoError(t, err, "Error should be nil when parsing the JWT")
|
||||
// assert.NotNil(t, claims, "Claims should not be nil")
|
||||
|
||||
// // Verify that the claims match the user and other values
|
||||
// assert.Equal(t, strconv.Itoa(int(user.ID)), claims.UserId, "User ID should match")
|
||||
// assert.Equal(t, "github.com/lafetz/snippitstash", claims.Issuer, "Issuer should match")
|
||||
// assert.Equal(t, "fortune.com", claims.Audience[0], "Audience should match")
|
||||
// assert.True(t, claims.ExpiresAt.Time.After(time.Now()), "Token should not be expired yet")
|
||||
|
||||
// // Ensure the parsing fails when using an invalid token
|
||||
// invalidToken := tokenString + "invalid"
|
||||
// _, err = ParseJwt(invalidToken, secretKey)
|
||||
// assert.Error(t, err, "Parsing an invalid token should return an error")
|
||||
// }
|
||||
// func TestParseJwte(t *testing.T) {
|
||||
// // Define user and key
|
||||
// user := &domain.User{ID: 1}
|
||||
// key := "secretkey"
|
||||
|
||||
// // Test valid token (not expired)
|
||||
// validJwt, err := CreateJwt(user, key, 4) // Set expiry to 10 seconds
|
||||
// assert.NoError(t, err)
|
||||
|
||||
// // Test if the token is parsed correctly
|
||||
// claims, err := ParseJwt(validJwt, key)
|
||||
// assert.NoError(t, err)
|
||||
// assert.Equal(t, "1", claims.UserId)
|
||||
|
||||
// // Wait for token to expire
|
||||
// time.Sleep(5 * time.Second) // Wait longer than the expiry time to test expiration
|
||||
|
||||
// // Test expired token
|
||||
// _, err = ParseJwt(validJwt, key)
|
||||
|
||||
// assert.Error(t, jwt.ErrTokenExpired) // Expect an error because the token should be expired
|
||||
// }
|
||||
Loading…
Reference in New Issue
Block a user