feat: 0.0.1

This commit is contained in:
FernandoJVideira committed 2026-01-08 01:15:07 +00:00
commit 961bdd81d4
146 files changed
+10444

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
package auth
import (
"errors"
"fmt"
"time"
"github.com/FernandoJVideira/simple_bank/internal/config"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
// JWTMaker is a JSON Web Token maker
type JWTMaker struct {
secretKey string
config config.Config
}
func NewJWTClaims(payload *Payload, config config.Config) jwt.Claims {
return &jwt.MapClaims{
"iat": jwt.NewNumericDate(payload.IssuedAt),
"iss": config.TokenISS,
"exp": jwt.NewNumericDate(payload.ExpiredAt),
"nbf": jwt.NewNumericDate(payload.IssuedAt),
"aud": []string{config.TokenAUD},
"sub": payload.Username,
"role": payload.Role,
"jti": payload.ID.String(),
}
}
// NewJWTMaker creates a new JWTMaker
func NewJWTMaker(config config.Config) (Maker, error) {
if len(config.TokenKey) < config.MinTokenSize {
return nil, fmt.Errorf("invalid key size: must be at least %d characters", config.MinTokenSize)
}
return &JWTMaker{secretKey: config.TokenKey, config: config}, nil
}
func (maker *JWTMaker) CreateToken(username string, role string, duration time.Duration) (string, *Payload, error) {
payload, err := NewPayload(username, role, duration)
if err != nil {
return "", nil, err
}
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, NewJWTClaims(payload, maker.config))
signedString, err := jwtToken.SignedString([]byte(maker.secretKey))
return signedString, payload, err
}
func (maker *JWTMaker) VerifyToken(token string) (*Payload, error) {
payloadClaims := jwt.MapClaims{}
keyFunc := func(token *jwt.Token) (any, error) {
_, ok := token.Method.(*jwt.SigningMethodHMAC)
if !ok {
return nil, ErrInvalidToken
}
return []byte(maker.secretKey), nil
}
jwtToken, err := jwt.ParseWithClaims(token, payloadClaims, keyFunc)
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrExpiredToken
} else if errors.Is(err, ErrInvalidToken) {
return nil, ErrInvalidToken
} else {
return nil, err
}
}
payloadClaims, ok := jwtToken.Claims.(jwt.MapClaims)
if !ok {
return nil, ErrInvalidToken
}
payload, err := parseTypes(payloadClaims)
if err != nil {
return nil, err
}
return payload, nil
}
func parseTypes(claims jwt.MapClaims) (*Payload, error) {
// Safe type assertions with error checking
jti, ok := claims["jti"].(string)
if !ok {
return nil, ErrInvalidToken
}
id, err := uuid.Parse(jti)
if err != nil {
return nil, ErrInvalidToken
}
username, ok := claims["sub"].(string)
if !ok {
return nil, ErrInvalidToken
}
role, ok := claims["role"].(string)
if !ok {
return nil, ErrInvalidToken
}
// Parse numeric dates
nbf, ok := claims["nbf"].(float64)
if !ok {
return nil, ErrInvalidToken
}
exp, ok := claims["exp"].(float64)
if !ok {
return nil, ErrInvalidToken
}
return &Payload{
ID: id,
Username: username,
Role: role,
IssuedAt: time.Unix(int64(nbf), 0),
ExpiredAt: time.Unix(int64(exp), 0),
}, nil
}
+83
View File
@@ -0,0 +1,83 @@
package auth
import (
"testing"
"time"
"github.com/FernandoJVideira/simple_bank/internal/config"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"github.com/FernandoJVideira/simple_bank/pkg/testutil"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
)
func TestJWTMaker(t *testing.T) {
config, err := config.LoadTestConfig("../../")
require.NoError(t, err)
maker, err := NewJWTMaker(config)
require.NoError(t, err)
username := testutil.RandomOwner()
duration := time.Minute
role := role.MemberRole
issuedAt := time.Now()
expiredAt := issuedAt.Add(duration)
token, payload, err := maker.CreateToken(username, role, duration)
require.NoError(t, err)
require.NotEmpty(t, token)
require.NotEmpty(t, payload)
payload, err = maker.VerifyToken(token)
require.NoError(t, err)
require.NotEmpty(t, payload)
require.NotZero(t, payload.ID)
require.Equal(t, username, payload.Username)
require.Equal(t, role, payload.Role)
require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second)
require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second)
}
func TestExpiredJWTToken(t *testing.T) {
config, err := config.LoadTestConfig("../../")
require.NoError(t, err)
maker, err := NewJWTMaker(config)
require.NoError(t, err)
username := testutil.RandomOwner()
duration := -time.Minute
token, payload, err := maker.CreateToken(username, role.MemberRole, duration)
require.NoError(t, err)
require.NotEmpty(t, token)
require.NotEmpty(t, payload)
payload, err = maker.VerifyToken(token)
require.Error(t, err)
require.EqualError(t, err, ErrExpiredToken.Error())
require.Nil(t, payload)
}
func TestInvalidJWTTokenAlgNone(t *testing.T) {
config, err := config.LoadTestConfig("../../")
require.NoError(t, err)
payload, err := NewPayload(testutil.RandomOwner(), role.MemberRole, time.Minute)
require.NoError(t, err)
jwtToken := jwt.NewWithClaims(jwt.SigningMethodNone, NewJWTClaims(payload, config))
token, err := jwtToken.SignedString(jwt.UnsafeAllowNoneSignatureType)
require.NoError(t, err)
maker, err := NewJWTMaker(config)
require.NoError(t, err)
payload, err = maker.VerifyToken(token)
require.Error(t, err)
require.EqualError(t, err, ErrInvalidToken.Error())
require.Nil(t, payload)
}
+11
View File
@@ -0,0 +1,11 @@
package auth
import "time"
// Maker is an interface for managing tokens
type Maker interface {
// CreateToken creates a new token for a specific username and duration
CreateToken(username string, role string, duration time.Duration) (string, *Payload, error)
// VerifyToken checks if the token is valid or not
VerifyToken(token string) (*Payload, error)
}
+97
View File
@@ -0,0 +1,97 @@
package auth
import (
"strings"
"time"
"aidanwoods.dev/go-paseto"
"github.com/FernandoJVideira/simple_bank/internal/config"
"github.com/google/uuid"
)
type PasetoMaker struct {
config config.Config
symmetricKey paseto.V4SymmetricKey
implicit []byte
}
func NewPasetoMaker(config config.Config) (Maker, error) {
maker := &PasetoMaker{
config: config,
symmetricKey: paseto.NewV4SymmetricKey(),
implicit: []byte("implicit"),
}
return maker, nil
}
func (maker *PasetoMaker) CreateToken(username string, role string, duration time.Duration) (string, *Payload, error) {
payload, err := NewPayload(username, role, duration)
if err != nil {
return "", nil, err
}
token := paseto.NewToken()
token.SetIssuedAt(payload.IssuedAt)
token.SetExpiration(payload.ExpiredAt)
token.SetString("role", payload.Role)
token.SetAudience(maker.config.TokenAUD)
token.SetIssuer(maker.config.TokenISS)
token.SetSubject(payload.Username)
token.SetJti(payload.ID.String())
signedToken := token.V4Encrypt(maker.symmetricKey, maker.implicit)
return signedToken, payload, nil
}
func (maker *PasetoMaker) VerifyToken(token string) (*Payload, error) {
parser := paseto.NewParser()
parser.AddRule(paseto.NotExpired())
parsedToken, err := parser.ParseV4Local(maker.symmetricKey, token, maker.implicit)
if err != nil {
if strings.Contains(err.Error(), "token has expired") {
return nil, ErrExpiredToken
}
return nil, ErrInvalidToken
}
payload, err := parseClaims(parsedToken)
if err != nil {
return nil, err
}
return payload, nil
}
func parseClaims(t *paseto.Token) (*Payload, error) {
id, err := t.GetJti()
if err != nil {
return nil, ErrInvalidToken
}
username, err := t.GetSubject()
if err != nil {
return nil, ErrInvalidToken
}
role, err := t.GetString("role")
if err != nil {
return nil, ErrInvalidToken
}
issuedAt, err := t.GetIssuedAt()
if err != nil {
return nil, ErrInvalidToken
}
expiredAt, err := t.GetExpiration()
if err != nil {
return nil, ErrInvalidToken
}
payload := &Payload{
ID: uuid.MustParse(id),
Username: username,
Role: role,
IssuedAt: issuedAt,
ExpiredAt: expiredAt,
}
return payload, nil
}
+62
View File
@@ -0,0 +1,62 @@
package auth
import (
"testing"
"time"
"github.com/FernandoJVideira/simple_bank/internal/config"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"github.com/FernandoJVideira/simple_bank/pkg/testutil"
"github.com/stretchr/testify/require"
)
func TestPasetoMaker(t *testing.T) {
config, err := config.LoadTestConfig("../../")
require.NoError(t, err)
maker, err := NewPasetoMaker(config)
require.NoError(t, err)
username := testutil.RandomOwner()
duration := time.Minute
issuedAt := time.Now()
expiredAt := issuedAt.Add(duration)
role := role.MemberRole
token, payload, err := maker.CreateToken(username, role, duration)
require.NoError(t, err)
require.NotEmpty(t, token)
require.NotEmpty(t, payload)
payload, err = maker.VerifyToken(token)
require.NoError(t, err)
require.NotEmpty(t, payload)
require.NotZero(t, payload.ID)
require.Equal(t, username, payload.Username)
require.Equal(t, role, payload.Role)
require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second)
require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second)
}
func TestExpiredPasetoToken(t *testing.T) {
config, err := config.LoadTestConfig("../../")
require.NoError(t, err)
maker, err := NewPasetoMaker(config)
require.NoError(t, err)
username := testutil.RandomOwner()
duration := -time.Minute
token, payload, err := maker.CreateToken(username, role.MemberRole, duration)
require.NoError(t, err)
require.NotEmpty(t, token)
require.NotEmpty(t, payload)
payload, err = maker.VerifyToken(token)
require.Error(t, err)
require.EqualError(t, err, ErrExpiredToken.Error())
require.Nil(t, payload)
}
+39
View File
@@ -0,0 +1,39 @@
package auth
import (
"fmt"
"time"
"github.com/google/uuid"
)
var (
ErrExpiredToken = fmt.Errorf("token has expired")
ErrInvalidToken = fmt.Errorf("token is invalid")
)
// Payload contains the payload data of the token
type Payload struct {
ID uuid.UUID `json:"id"`
Username string `json:"username"` // or UserID
Role string `json:"role"`
IssuedAt time.Time `json:"issued_at"`
ExpiredAt time.Time `json:"expired_at"`
}
// NewPayload creates a new token payload with a specific username and duration
func NewPayload(username string, role string, duration time.Duration) (*Payload, error) {
tokenID, err := uuid.NewRandom()
if err != nil {
return nil, err
}
payload := &Payload{
ID: tokenID,
Username: username,
Role: role,
IssuedAt: time.Now(),
ExpiredAt: time.Now().Add(duration),
}
return payload, nil
}