sso/internal/services/auth/auth.go

163 lines
4.1 KiB
Go
Raw Normal View History

2023-12-17 12:17:55 +02:00
package auth
import (
"context"
"errors"
"fmt"
"log/slog"
"sso/internal/domain/models"
"sso/internal/lib/jwt"
"sso/internal/lib/logger/sl"
"sso/internal/storage"
"time"
"golang.org/x/crypto/bcrypt"
)
type Auth struct {
log *slog.Logger
usrSaver UserSaver
usrProvider UserProvider
appProvider AppProvider
tokenTTL time.Duration
}
type UserSaver interface {
SaveUser(ctx context.Context, email string, passHash []byte) (uid int64, err error)
}
type UserProvider interface {
User(ctx context.Context, email string) (models.User, error)
IsAdmin(ctx context.Context, userID int64) (bool, error)
}
type AppProvider interface {
App(ctx context.Context, appID int) (models.App, error)
}
var (
ErrInvalidCredentials = errors.New("invalid credentials")
ErrInvalidAppID = errors.New("invalid app id")
ErrUserExists = errors.New("user already exists")
ErrUserNotFound = errors.New("user not found")
)
// New returns a new instance of the Auth service.
func New(log *slog.Logger, userSaver UserSaver, userProvider UserProvider, appProvider AppProvider, tokenTTL time.Duration) *Auth {
return &Auth{
log: log,
usrSaver: userSaver,
usrProvider: userProvider,
appProvider: appProvider,
tokenTTL: tokenTTL,
}
}
// Login checks if user with given credentials exists in the system and returns access token.
//
// If user exists, but password is incorrect, returns error.
// If user doesn't exist, returns error.
func (a *Auth) Login(ctx context.Context, email string, password string, appID int) (string, error) {
const op = "services.auth.Login"
log := a.log.With(
slog.String("op", op),
slog.String("username", email),
)
log.Info("attempting to login user")
user, err := a.usrProvider.User(ctx, email)
if err != nil {
if errors.Is(err, storage.ErrUserNotFound) {
a.log.Warn("user not found", sl.Err(err))
return "", fmt.Errorf("%s: %w", op, ErrInvalidCredentials)
}
a.log.Error("failed to get user", sl.Err(err))
return "", fmt.Errorf("%s: %w", op, err)
}
if err := bcrypt.CompareHashAndPassword(user.PassHash, []byte(password)); err != nil {
a.log.Info("invalid credentials", sl.Err(err))
return "", fmt.Errorf("%s: %w", op, ErrInvalidCredentials)
}
app, err := a.appProvider.App(ctx, appID)
if err != nil {
return "", fmt.Errorf("%s: %w", op, err)
}
log.Info("user logged in successfully")
token, err := jwt.NewToken(user, app.ID, app.Secret, a.tokenTTL)
if err != nil {
a.log.Error("failed to generate token", sl.Err(err))
return "", fmt.Errorf("%s: %w", op, err)
}
return token, nil
}
// RegisterNewUser registers new user in the system and returns user ID.
// If user with given username already exists, returns error.
func (a *Auth) RegisterNewUser(ctx context.Context, email string, pass string) (int64, error) {
const op = "services.auth.RegisterNewUser"
log := a.log.With(
slog.String("op", op),
slog.String("email", email),
)
log.Info("registering user")
passHash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
if err != nil {
log.Error("failed to generate password hash", sl.Err(err))
return 0, fmt.Errorf("%s: %w", op, err)
}
id, err := a.usrSaver.SaveUser(ctx, email, passHash)
if err != nil {
if errors.Is(err, storage.ErrUserExists) {
log.Warn("user already exists", sl.Err(err))
return 0, fmt.Errorf("%s: %w", op, ErrUserExists)
}
log.Error("failed to save user", sl.Err(err))
return 0, fmt.Errorf("%s: %w", op, err)
}
log.Info("user registred")
return id, nil
}
// IsAdmin checks if user is admin
func (a *Auth) IsAdmin(ctx context.Context, userID int64) (bool, error) {
const op = "services.auth.IsAdmin"
log := a.log.With(
slog.String("op", op),
slog.Int64("user_id", userID),
)
log.Info("checking if user is admin")
isAdmin, err := a.usrProvider.IsAdmin(ctx, userID)
if err != nil {
if errors.Is(err, storage.ErrAppNotFound) {
log.Warn("app not found", sl.Err(err))
return false, fmt.Errorf("%s: %w", op, ErrInvalidAppID)
}
return false, fmt.Errorf("%s: %w", op, err)
}
log.Info("checking if user is admin", slog.Bool("is_admin", isAdmin))
return isAdmin, nil
}