Files
sxchnge/backend/product/auth/http/v1/public/validate.go
T

107 lines
3.8 KiB
Go

package public
import (
"context"
"errors"
customermodel "git.sendico.io/tech/sxchnge/product/customers/model"
sessionmodel "git.sendico.io/tech/sxchnge/product/sessions/model"
"net/http"
"time"
"git.sendico.io/tech/sxchnge/product/auth/http/v1/request"
"git.sendico.io/tech/sxchnge/product/auth/http/v1/response"
"git.sendico.io/tech/sxchnge/product/sessions/browser"
"git.sendico.io/tech/sxchnge/shared/errors"
"git.sendico.io/tech/sxchnge/shared/logging/fields"
"go.uber.org/zap"
)
var errClientIPNotAllowed = merrors.Define("client_ip_not_allowed")
func isExpectedRefreshValidationError(err error) bool {
return errors.Is(err, merrors.ErrUnauthorized)
}
func validateToken(token string, rt *sessionmodel.RefreshToken) string {
if rt.AccountRef == nil {
return "missing account reference"
}
if token != rt.RefreshToken {
return "tokens do not match"
}
if rt.ExpiresAt.Before(time.Now()) {
return "token expired"
}
if rt.IsRevoked {
return "token has been revoked"
}
return ""
}
func (pr *PublicRouter) validateRefreshToken(ctx context.Context, r *http.Request, req *srequest.TokenRefreshRotate) (*customermodel.Account, *sresponse.TokenData, *sessionmodel.RefreshToken, error) {
credential, err := browser.RefreshCredential(r, req.RefreshToken)
if err != nil {
return nil, nil, nil, err
}
req.RefreshToken = credential
client, err := pr.rtdb.GetClient(ctx, req.ClientID)
if errors.Is(err, merrors.ErrNoData) || client == nil {
pr.logger.Debug("Refresh token rejected: client not found", zap.String("client_id", req.ClientID))
return nil, nil, nil, merrors.Unauthorized("client not found")
}
if err != nil {
pr.logger.Warn("Failed to fetch client for refresh token validation", zap.Error(err), zap.String("client_id", req.ClientID))
return nil, nil, nil, err
}
if client.IsRevoked {
return nil, nil, nil, merrors.Unauthorized("client has been revoked")
}
if h := pr.validateClientIPPolicy(r, req.ClientID, client); h != nil {
return nil, nil, nil, errClientIPNotAllowed
}
rt, err := pr.rtdb.GetByCRT(ctx, req)
if errors.Is(err, merrors.ErrNoData) || errors.Is(err, merrors.ErrAccessDenied) {
pr.logger.Debug("Refresh session is inactive", zap.String("client_id", req.ClientID), zap.String("device_id", req.DeviceID))
return nil, nil, nil, merrors.Unauthorized("refresh session is inactive")
}
if err != nil {
return nil, nil, nil, err
}
if rt.SessionVersion == "" {
pr.logger.Debug("Refresh token rejected: legacy session requires reauthentication",
zap.String("client_id", req.ClientID), zap.String("device_id", req.DeviceID))
return nil, nil, nil, merrors.Unauthorized("session reauthentication required")
}
if reason := validateToken(req.RefreshToken, rt); len(reason) > 0 {
pr.logger.Debug("Token validation failed", zap.String("reason", reason))
return nil, nil, nil, merrors.Unauthorized(reason)
}
var account customermodel.Account
if err := pr.db.Get(ctx, *rt.AccountRef, &account); errors.Is(err, merrors.ErrNoData) {
pr.logger.Debug("User not found while rotating refresh token", zap.Error(err), mzap.AccRef(*rt.AccountRef))
return nil, nil, nil, merrors.Unauthorized("user not found")
} else if err != nil {
pr.logger.Warn("Failed to fetch user while validating refresh token", zap.Error(err), mzap.AccRef(*rt.AccountRef))
return nil, nil, nil, err
}
if !account.IsActive() || account.IsArchived() {
return nil, nil, nil, merrors.Unauthorized("customer is inactive")
}
if client.AccountRef != nil && *client.AccountRef != account.ID {
return nil, nil, nil, merrors.Unauthorized("client account mismatch")
}
accessToken, err := pr.imp.CreateAccessTokenForSession(&account, rt.SessionIdentifier)
if err != nil {
pr.logger.Warn("Failed to generate access token", zap.Error(err))
return nil, nil, nil, err
}
return &account, &accessToken, rt, nil
}