Extract Claims struct from auth/types.go into dedicated identity package for better separation of concerns. Update all imports and usages across auth service, token handling, and request context utilities.
158 lines
4.3 KiB
Go
158 lines
4.3 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nexavpn/nexavpn/backend/internal/identity"
|
|
)
|
|
|
|
var ErrInvalidCredentials = errors.New("invalid credentials")
|
|
|
|
type UserRecord struct {
|
|
ID uuid.UUID
|
|
Username string
|
|
DisplayName string
|
|
Role string
|
|
PasswordHash string
|
|
IsActive bool
|
|
}
|
|
|
|
type Repository interface {
|
|
FindUserByUsername(ctx context.Context, username string) (UserRecord, error)
|
|
CreateSession(ctx context.Context, userID uuid.UUID, expiresAt time.Time, ipAddress string, userAgent string) (uuid.UUID, error)
|
|
StoreRefreshToken(ctx context.Context, sessionID uuid.UUID, userID uuid.UUID, tokenHash string, expiresAt time.Time) error
|
|
FindRefreshToken(ctx context.Context, tokenHash string) (UserRecord, uuid.UUID, error)
|
|
RevokeRefreshToken(ctx context.Context, tokenHash string) error
|
|
HasUsers(ctx context.Context) (bool, error)
|
|
CreateBootstrapAdmin(ctx context.Context, username, displayName, passwordHash string) (UserRecord, error)
|
|
}
|
|
|
|
type Service struct {
|
|
repo Repository
|
|
jwtSecret string
|
|
jwtIssuer string
|
|
accessTokenTTL time.Duration
|
|
refreshTokenTTL time.Duration
|
|
}
|
|
|
|
func NewService(repo Repository, jwtSecret, jwtIssuer string, accessTokenTTL, refreshTokenTTL time.Duration) *Service {
|
|
return &Service{
|
|
repo: repo,
|
|
jwtSecret: jwtSecret,
|
|
jwtIssuer: jwtIssuer,
|
|
accessTokenTTL: accessTokenTTL,
|
|
refreshTokenTTL: refreshTokenTTL,
|
|
}
|
|
}
|
|
|
|
func (s *Service) Login(ctx context.Context, username, password, ipAddress, userAgent string) (LoginResponse, error) {
|
|
record, err := s.repo.FindUserByUsername(ctx, username)
|
|
if err != nil || !record.IsActive || !VerifyPassword(record.PasswordHash, password) {
|
|
return LoginResponse{}, ErrInvalidCredentials
|
|
}
|
|
|
|
sessionID, err := s.repo.CreateSession(ctx, record.ID, time.Now().Add(s.refreshTokenTTL), ipAddress, userAgent)
|
|
if err != nil {
|
|
return LoginResponse{}, err
|
|
}
|
|
|
|
plainRefresh, hashedRefresh, err := NewRefreshToken()
|
|
if err != nil {
|
|
return LoginResponse{}, err
|
|
}
|
|
|
|
if err := s.repo.StoreRefreshToken(ctx, sessionID, record.ID, hashedRefresh, time.Now().Add(s.refreshTokenTTL)); err != nil {
|
|
return LoginResponse{}, err
|
|
}
|
|
|
|
access, err := SignAccessToken(s.jwtSecret, s.jwtIssuer, s.accessTokenTTL, identity.Claims{
|
|
UserID: record.ID,
|
|
Username: record.Username,
|
|
Role: record.Role,
|
|
Session: sessionID,
|
|
})
|
|
if err != nil {
|
|
return LoginResponse{}, err
|
|
}
|
|
|
|
return LoginResponse{
|
|
AccessToken: access,
|
|
RefreshToken: plainRefresh,
|
|
ExpiresIn: int64(s.accessTokenTTL.Seconds()),
|
|
User: UserView{
|
|
ID: record.ID,
|
|
Username: record.Username,
|
|
DisplayName: record.DisplayName,
|
|
Role: record.Role,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) Refresh(ctx context.Context, refreshToken string) (LoginResponse, error) {
|
|
record, sessionID, err := s.repo.FindRefreshToken(ctx, hashToken(refreshToken))
|
|
if err != nil {
|
|
return LoginResponse{}, ErrInvalidCredentials
|
|
}
|
|
|
|
access, err := SignAccessToken(s.jwtSecret, s.jwtIssuer, s.accessTokenTTL, identity.Claims{
|
|
UserID: record.ID,
|
|
Username: record.Username,
|
|
Role: record.Role,
|
|
Session: sessionID,
|
|
})
|
|
if err != nil {
|
|
return LoginResponse{}, err
|
|
}
|
|
|
|
return LoginResponse{
|
|
AccessToken: access,
|
|
RefreshToken: refreshToken,
|
|
ExpiresIn: int64(s.accessTokenTTL.Seconds()),
|
|
User: UserView{
|
|
ID: record.ID,
|
|
Username: record.Username,
|
|
DisplayName: record.DisplayName,
|
|
Role: record.Role,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) Logout(ctx context.Context, refreshToken string) error {
|
|
return s.repo.RevokeRefreshToken(ctx, hashToken(refreshToken))
|
|
}
|
|
|
|
func (s *Service) BootstrapAdmin(ctx context.Context, username, displayName, password string) (UserView, error) {
|
|
hasUsers, err := s.repo.HasUsers(ctx)
|
|
if err != nil {
|
|
return UserView{}, err
|
|
}
|
|
if hasUsers {
|
|
return UserView{}, errors.New("bootstrap already completed")
|
|
}
|
|
|
|
passwordHash, err := HashPassword(password)
|
|
if err != nil {
|
|
return UserView{}, err
|
|
}
|
|
|
|
record, err := s.repo.CreateBootstrapAdmin(ctx, username, displayName, passwordHash)
|
|
if err != nil {
|
|
return UserView{}, err
|
|
}
|
|
|
|
return UserView{
|
|
ID: record.ID,
|
|
Username: record.Username,
|
|
DisplayName: record.DisplayName,
|
|
Role: record.Role,
|
|
}, nil
|
|
}
|
|
|
|
func hashToken(plain string) string {
|
|
return base64Hash(plain)
|
|
}
|