create auth package
This commit is contained in:
246
internal/auth/service.go
Normal file
246
internal/auth/service.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"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"
|
||||
"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.Service
|
||||
token *token.Service
|
||||
}
|
||||
}
|
||||
|
||||
type AuthServiceDependencies struct {
|
||||
Configuration *config.Config
|
||||
|
||||
JWTService *jwt.Service
|
||||
UserService *user.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.Service
|
||||
token *token.Service
|
||||
}{
|
||||
jwt: deps.JWTService,
|
||||
user: deps.UserService,
|
||||
token: deps.TokenService,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Users
|
||||
|
||||
func (s *Service) Get(by, value string) (*user.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.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.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 {
|
||||
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, fmt.Errorf("invalid password: %w", err)
|
||||
}
|
||||
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 {
|
||||
return s.services.token.RevokeByRefreshDefault(rjti)
|
||||
}
|
||||
|
||||
// Access tokens
|
||||
|
||||
// ValidateAccessToken validates the given access token string.
|
||||
// Returns the user ID (sub claim) if valid, or an error.
|
||||
func (s *Service) ValidateAccessToken(tokenStr string) (int64, error) {
|
||||
claims, _, err := s.services.jwt.Validate(tokenStr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to validate access token: %w", err)
|
||||
}
|
||||
|
||||
isRevoked, err := s.services.token.IsRevoked(claims["rjti"].(string))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to check if token is revoked: %w", err)
|
||||
}
|
||||
if isRevoked {
|
||||
return 0, fmt.Errorf("token is revoked")
|
||||
}
|
||||
|
||||
sub := claims["sub"].(float64)
|
||||
return int64(sub), 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.
|
||||
func (s *Service) RefreshTokens(refreshTokenStr string) (*Tokens, error) {
|
||||
claims, rjti, err := s.services.jwt.Validate(refreshTokenStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to validate refresh token: %w", 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, fmt.Errorf("refresh token is revoked")
|
||||
}
|
||||
|
||||
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 user id and error.
|
||||
func (s *Service) ValidateRefreshToken(tokenStr string) (int64, error) {
|
||||
claims, _, err := s.services.jwt.Validate(tokenStr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to validate refresh token: %w", err)
|
||||
}
|
||||
|
||||
isRevoked, err := s.services.token.IsRevoked(claims["jti"].(string))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to check if token is revoked: %w", err)
|
||||
}
|
||||
if isRevoked {
|
||||
return 0, fmt.Errorf("refresh token is revoked")
|
||||
}
|
||||
|
||||
sub := claims["sub"].(float64)
|
||||
return int64(sub), 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, fmt.Errorf("token is missing")
|
||||
}
|
||||
if !strings.HasPrefix(header, "Bearer ") {
|
||||
return nil, fmt.Errorf("token is missing")
|
||||
}
|
||||
tokenString := strings.TrimPrefix(header, "Bearer ")
|
||||
tokenClaims, _, err := s.services.jwt.Validate(tokenString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tokenClaims, nil
|
||||
}
|
||||
Reference in New Issue
Block a user