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 }