package authorized import ( "errors" customermodel "git.sendico.io/tech/sxchnge/product/customers/model" "net/http" "strings" "git.sendico.io/tech/sxchnge/product/auth/ipguard" emodel "git.sendico.io/tech/sxchnge/product/auth/model" "git.sendico.io/tech/sxchnge/product/auth/response" "git.sendico.io/tech/sxchnge/product/sessions/metrics" "git.sendico.io/tech/sxchnge/shared/errors" api "git.sendico.io/tech/sxchnge/shared/http" "git.sendico.io/tech/sxchnge/shared/http/response" "git.sendico.io/tech/sxchnge/shared/logging/fields" "git.sendico.io/tech/sxchnge/shared/service" "github.com/go-chi/jwtauth/v5" "go.uber.org/zap" ) type tokenHandlerFunc = func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc func (ar *AuthorizedRouter) validateClientPolicy(r *http.Request, t *emodel.AccountToken) http.HandlerFunc { clientID := strings.TrimSpace(t.ClientID) if clientID == "" { // Legacy tokens without client_id remain valid until expiration. return nil } client, err := ar.rtdb.GetClient(r.Context(), clientID) if errors.Is(err, merrors.ErrNoData) || client == nil { ar.logger.Debug("Client not found for access token", zap.String("client_id", clientID)) return response.Unauthorized(ar.logger, ar.service, "client not found") } if err != nil { ar.logger.Warn("Failed to resolve client for access token", zap.Error(err), zap.String("client_id", clientID)) return response.Internal(ar.logger, ar.service, err) } if client.IsRevoked { return response.Unauthorized(ar.logger, ar.service, "client has been revoked") } if client.AccountRef != nil && *client.AccountRef != t.AccountRef { return response.Unauthorized(ar.logger, ar.service, "client account mismatch") } clientIP := ipguard.ClientIP(r) allowed, err := ipguard.Allowed(clientIP, client.AllowedCIDRs) if err != nil { ar.logger.Warn("Client IP policy contains invalid CIDR", zap.Error(err), zap.String("client_id", clientID)) return response.Forbidden(ar.logger, ar.service, "client_ip_policy_invalid", "client ip policy is invalid") } if !allowed { rawIP := "" if clientIP != nil { rawIP = clientIP.String() } ar.logger.Info("Client IP policy denied authorized request", zap.String("client_id", clientID), zap.String("remote_ip", rawIP)) return response.Forbidden(ar.logger, ar.service, "ip_not_allowed", "request ip is not allowed for this client") } return nil } func (ar *AuthorizedRouter) validateSession(r *http.Request, t *emodel.AccountToken) http.HandlerFunc { if t.Pending { return nil } if strings.TrimSpace(t.ClientID) == "" || strings.TrimSpace(t.DeviceID) == "" || strings.TrimSpace(t.SessionVersion) == "" { sessionmetrics.Observe(sessionmetrics.Validation, sessionmetrics.MissingIdentity) ar.logger.Debug("Access token has no session identifier", mzap.AccRef(t.AccountRef)) return response.Unauthorized(ar.logger, ar.service, "session identifier is required") } if _, err := ar.rtdb.GetSession(r.Context(), t.AccountRef, &t.SessionIdentifier); err != nil { if errors.Is(err, merrors.ErrNoData) || errors.Is(err, merrors.ErrAccessDenied) { sessionmetrics.Observe(sessionmetrics.Validation, sessionmetrics.AlreadyInactive) ar.logger.Debug("Access token session is not active", mzap.AccRef(t.AccountRef), zap.String("client_id", t.ClientID), zap.String("device_id", t.DeviceID)) return response.Unauthorized(ar.logger, ar.service, "session is not active") } sessionmetrics.Observe(sessionmetrics.Validation, sessionmetrics.StorageError) ar.logger.Warn("Failed to validate access token session", zap.Error(err), mzap.AccRef(t.AccountRef), zap.String("client_id", t.ClientID), zap.String("device_id", t.DeviceID)) return response.Internal(ar.logger, ar.service, err) } sessionmetrics.Observe(sessionmetrics.Validation, sessionmetrics.Active) return nil } func (ar *AuthorizedRouter) tokenHandlerWithSessionCheck(service mservice.Type, endpoint string, method api.HTTPMethod, checkSession bool, handler tokenHandlerFunc) { hndlr := func(r *http.Request) http.HandlerFunc { _, claims, err := jwtauth.FromContext(r.Context()) if err != nil { ar.logger.Debug("Authorization failed", zap.Error(err), zap.String("request", r.URL.Path)) return response.Unauthorized(ar.logger, ar.service, "credentials required") } t, err := emodel.Claims2Token(claims) if err != nil { ar.logger.Debug("Failed to decode account token", zap.Error(err)) return response.BadRequest(ar.logger, ar.service, "credentials_unreadable", "faild to parse credentials") } if h := ar.validateClientPolicy(r, t); h != nil { return h } if checkSession { if h := ar.validateSession(r, t); h != nil { return h } } return handler(r, t) } ar.imp.InstallHandler(service, endpoint, method, hndlr) } func (ar *AuthorizedRouter) tokenHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler tokenHandlerFunc) { ar.tokenHandlerWithSessionCheck(service, endpoint, method, true, handler) } func (ar *AuthorizedRouter) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) { hndlr := func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc { if t.Pending { return response.Unauthorized(ar.logger, ar.service, "additional verification required") } var a customermodel.Account if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil { if errors.Is(err, merrors.ErrNoData) { ar.logger.Debug("Failed to find related user", zap.Error(err), mzap.AccRef(t.AccountRef)) return response.NotFound(ar.logger, ar.service, err.Error()) } return response.Internal(ar.logger, ar.service, err) } if !a.IsActive() || a.IsArchived() { return response.Unauthorized(ar.logger, ar.service, "customer is inactive") } accessToken, err := ar.imp.CreateAccessTokenForSession(&a, t.SessionIdentifier) if err != nil { ar.logger.Warn("Failed to generate access token", zap.Error(err)) return response.Internal(ar.logger, ar.service, err) } return handler(r, &a, &accessToken) } ar.tokenHandler(service, endpoint, method, hndlr) } func (ar *AuthorizedRouter) PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) { hndlr := func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc { var a customermodel.Account if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil { if errors.Is(err, merrors.ErrNoData) { ar.logger.Debug("Failed to find related user", zap.Error(err), mzap.AccRef(t.AccountRef)) return response.NotFound(ar.logger, ar.service, err.Error()) } return response.Internal(ar.logger, ar.service, err) } if a.Status == customermodel.AccountBlocked || a.IsArchived() { return response.Unauthorized(ar.logger, ar.service, "customer is inactive") } return handler(r, &a, t) } ar.tokenHandler(service, endpoint, method, hndlr) }