auth services, middleware, and other stuff
This commit is contained in:
90
internal/auth/service/service.go
Normal file
90
internal/auth/service/service.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/auth"
|
||||
userrepository "gitea.michaelthomson.dev/mthomson/habits/internal/user/repository"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound error = errors.New("user cannot be found")
|
||||
ErrUnauthorized error = errors.New("user password incorrect")
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
Create(ctx context.Context, user userrepository.UserRow) (userrepository.UserRow, error)
|
||||
GetByEmail(ctx context.Context, email string) (userrepository.UserRow, error)
|
||||
}
|
||||
|
||||
type AuthService struct {
|
||||
logger *slog.Logger
|
||||
jwtKey []byte
|
||||
userRepository UserRepository
|
||||
argon2IdHash *auth.Argon2IdHash
|
||||
}
|
||||
|
||||
func NewAuthService(logger *slog.Logger, jwtKey []byte, userRepository UserRepository, argon2IdHash *auth.Argon2IdHash) *AuthService {
|
||||
return &AuthService{
|
||||
logger: logger,
|
||||
jwtKey: jwtKey,
|
||||
userRepository: userRepository,
|
||||
argon2IdHash: argon2IdHash,
|
||||
}
|
||||
}
|
||||
|
||||
func (a AuthService) Login(ctx context.Context, email string, password string) (string, error) {
|
||||
// get user if exists
|
||||
userRow, err := a.userRepository.GetByEmail(ctx, email)
|
||||
if err != nil {
|
||||
if err == userrepository.ErrNotFound {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
|
||||
a.logger.ErrorContext(ctx, err.Error())
|
||||
return "", err
|
||||
}
|
||||
|
||||
// compare hashed passswords
|
||||
err = a.argon2IdHash.Compare(userRow.HashedPassword, userRow.Salt, []byte(password))
|
||||
|
||||
if err == auth.ErrNoMatch {
|
||||
return "", ErrUnauthorized
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
a.logger.ErrorContext(ctx, err.Error())
|
||||
return "", err
|
||||
}
|
||||
|
||||
// create token and return it
|
||||
token, err := a.CreateToken(ctx, email)
|
||||
if err != nil {
|
||||
a.logger.ErrorContext(ctx, err.Error())
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (a AuthService) CreateToken(ctx context.Context, email string) (string, error) {
|
||||
// Create a new JWT token with claims
|
||||
claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": email,
|
||||
"iss": "todo-app",
|
||||
"aud": "user",
|
||||
"exp": time.Now().Add(time.Hour).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
})
|
||||
|
||||
tokenString, err := claims.SignedString(a.jwtKey)
|
||||
if err != nil {
|
||||
a.logger.ErrorContext(ctx, err.Error())
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
81
internal/auth/service/service_test.go
Normal file
81
internal/auth/service/service_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/auth"
|
||||
authservice "gitea.michaelthomson.dev/mthomson/habits/internal/auth/service"
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/test"
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/user/repository"
|
||||
userservice "gitea.michaelthomson.dev/mthomson/habits/internal/user/service"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
logger := slog.Default()
|
||||
tdb := test.NewTestDatabase(t)
|
||||
defer tdb.TearDown()
|
||||
userRepository := repository.NewUserRepository(logger, tdb.Db)
|
||||
argon2IdHash := auth.NewArgon2IdHash(1, 32, 64*1024, 32, 256)
|
||||
|
||||
userService := userservice.NewUserService(logger, userRepository, argon2IdHash)
|
||||
authService := authservice.NewAuthService(logger, []byte("secretkey"), userRepository, argon2IdHash)
|
||||
|
||||
_, err := userService.Register(ctx, "test@test.com", "supersecurepassword")
|
||||
AssertNoError(t, err)
|
||||
|
||||
t.Run("login existing user with correct credentials", func(t *testing.T) {
|
||||
want, err := authService.CreateToken(ctx, "test@test.com")
|
||||
AssertNoError(t, err)
|
||||
got, err := authService.Login(ctx, "test@test.com", "supersecurepassword")
|
||||
|
||||
AssertNoError(t, err)
|
||||
AssertTokens(t, got, want)
|
||||
})
|
||||
|
||||
t.Run("login existing user with incorrect credentials", func(t *testing.T) {
|
||||
_, err := authService.Login(ctx, "test@test.com", "superwrongpassword")
|
||||
|
||||
AssertErrors(t, err, authservice.ErrUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("login nonexistant user", func(t *testing.T) {
|
||||
_, err := authService.Login(ctx, "foo@test.com", "supersecurepassword")
|
||||
|
||||
AssertErrors(t, err, authservice.ErrNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func AssertErrors(t testing.TB, got, want error) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("got error: %v, want error: %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func AssertNoError(t testing.TB, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func AssertTokens(t testing.TB, got, want string) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("expected matching tokens, got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func NewUUID(t testing.TB) uuid.UUID {
|
||||
t.Helper()
|
||||
uuid, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
t.Errorf("error generation uuid: %v", err)
|
||||
}
|
||||
return uuid
|
||||
}
|
||||
Reference in New Issue
Block a user