package auth import ( "errors" "fmt" "net/http" "strings" "git.oblat.lv/alex/triggerssmith/internal/config" "git.oblat.lv/alex/triggerssmith/internal/jwt" "git.oblat.lv/alex/triggerssmith/internal/token" user_p "git.oblat.lv/alex/triggerssmith/internal/user" ejwt "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) type Tokens struct { Access string Refresh string } type Service struct { cfg *config.Config services struct { jwt *jwt.Service user *user_p.Service token *token.Service } } type AuthServiceDependencies struct { Configuration *config.Config JWTService *jwt.Service UserService *user_p.Service TokenService *token.Service } func NewAuthService(deps AuthServiceDependencies) (*Service, error) { if deps.Configuration == nil { return nil, fmt.Errorf("config is nil") } if deps.JWTService == nil { return nil, fmt.Errorf("jwt service is nil") } if deps.UserService == nil { return nil, fmt.Errorf("user service is nil") } if deps.TokenService == nil { return nil, fmt.Errorf("token service is nil") } return &Service{ cfg: deps.Configuration, services: struct { jwt *jwt.Service user *user_p.Service token *token.Service }{ jwt: deps.JWTService, user: deps.UserService, token: deps.TokenService, }, }, nil } // Users func (s *Service) Get(by, value string) (*user_p.User, error) { return s.services.user.GetBy(by, value) } // Register creates a new user with the given username, email, and password. // Password is hashed before storing. // Returns the created user or an error. func (s *Service) Register(username, email, password string) (*user_p.User, error) { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return nil, fmt.Errorf("failed to hash password: %w", err) } user := &user_p.User{ Username: username, Email: email, Password: string(hashedPassword), } err = s.services.user.Create(user) if err != nil { return nil, fmt.Errorf("failed to create user: %w", err) } return user, nil } // Login authenticates a user with the given username and password. // Returns access and refresh tokens if successful. func (s *Service) Login(username, password string) (*Tokens, error) { user, err := s.services.user.GetBy("username", username) if err != nil { if err == user_p.ErrUserNotFound { return nil, ErrInvalidUsername } return nil, fmt.Errorf("failed to get user by username: %w", err) } err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) if err != nil { return nil, ErrInvalidPassword } refreshToken, rjti, err := s.services.jwt.Generate(s.cfg.Auth.RefreshTokenTTL, ejwt.MapClaims{ "sub": user.ID, }) if err != nil { return nil, fmt.Errorf("failed to generate refresh token: %w", err) } accessToken, _, err := s.services.jwt.Generate(s.cfg.Auth.AccessTokenTTL, ejwt.MapClaims{ "sub": user.ID, "rjti": rjti, }) if err != nil { return nil, fmt.Errorf("failed to generate refresh token: %w", err) } return &Tokens{Access: accessToken, Refresh: refreshToken}, nil } // Logout revokes the refresh token identified by the given rjti. func (s *Service) Logout(rjti string) error { err := s.services.token.RevokeByRefreshDefault(rjti) if err != nil { if errors.Is(err, token.ErrTokenIsRevoked) { return ErrInvalidToken } return fmt.Errorf("failed to revoke token: %w", err) } return nil } // Access tokens // ValidateAccessToken validates the given access token string. // Returns claims if valid, or an error. func (s *Service) ValidateAccessToken(tokenStr string) (ejwt.Claims, error) { claims, _, err := s.services.jwt.Validate(tokenStr) if err != nil { return nil, fmt.Errorf("failed to validate access token: %w", err) } isRevoked, err := s.services.token.IsRevoked(claims["rjti"].(string)) if err != nil { return nil, fmt.Errorf("failed to check if token is revoked: %w", err) } if isRevoked { return nil, fmt.Errorf("token is revoked") } return claims, nil } // Refresh tokens // RefreshTokens validates the given refresh token and issues new access and refresh tokens. // Returns the new access and refresh tokens or an error. // May return [ErrInvalidToken] if the refresh token is invalid or revoked. func (s *Service) RefreshTokens(refreshTokenStr string) (*Tokens, error) { claims, rjti, err := s.services.jwt.Validate(refreshTokenStr) if err != nil { return nil, errors.Join(ErrInvalidToken, err) } isRevoked, err := s.services.token.IsRevoked(rjti) if err != nil { return nil, fmt.Errorf("failed to check if token is revoked: %w", err) } if isRevoked { return nil, ErrInvalidToken } sub := claims["sub"].(float64) newRefreshToken, newRjti, err := s.services.jwt.Generate(s.cfg.Auth.RefreshTokenTTL, ejwt.MapClaims{ "sub": sub, }) if err != nil { return nil, fmt.Errorf("failed to generate new refresh token: %w", err) } newAccessToken, _, err := s.services.jwt.Generate(s.cfg.Auth.AccessTokenTTL, ejwt.MapClaims{ "sub": sub, "rjti": newRjti, }) if err != nil { return nil, fmt.Errorf("failed to generate new access token: %w", err) } // Revoke the old refresh token if err := s.services.token.RevokeByRefreshDefault(rjti); err != nil { return nil, fmt.Errorf("failed to revoke old refresh token: %w", err) } return &Tokens{Access: newAccessToken, Refresh: newRefreshToken}, nil } // ValidateRefreshToken validates the given refresh token string. // Returns claims and error. func (s *Service) ValidateRefreshToken(tokenStr string) (ejwt.Claims, error) { claims, _, err := s.services.jwt.Validate(tokenStr) if err != nil { return nil, fmt.Errorf("failed to validate refresh token: %w", err) } isRevoked, err := s.services.token.IsRevoked(claims["jti"].(string)) if err != nil { return nil, fmt.Errorf("failed to check if token is revoked: %w", err) } if isRevoked { return nil, fmt.Errorf("refresh token is revoked") } return claims, nil } // RevokeRefresh revokes the refresh token identified by the given token string. func (s *Service) RevokeRefresh(token string) error { _, rjti, err := s.services.jwt.Validate(token) if err != nil { return fmt.Errorf("failed to validate refresh token: %w", err) } return s.services.token.RevokeByRefreshDefault(rjti) } // IsRefreshRevoked checks if the refresh token identified by the given token string is revoked. func (s *Service) IsRefreshRevoked(token string) (bool, error) { _, rjti, err := s.services.jwt.Validate(token) if err != nil { return false, fmt.Errorf("failed to validate refresh token: %w", err) } return s.services.token.IsRevoked(rjti) } func (s *Service) AuthenticateRequest(r *http.Request) (ejwt.Claims, error) { header := r.Header.Get("Authorization") if header == "" { return nil, ErrTokenIsMissing } if !strings.HasPrefix(header, "Bearer ") { return nil, ErrTokenIsMissing } tokenString := strings.TrimPrefix(header, "Bearer ") tokenClaims, _, err := s.services.jwt.Validate(tokenString) if err != nil { return nil, err } return tokenClaims, nil }