Skip to content

[feature] Allow users joining different organizations having different identity verification rules #692

@nemesifier

Description

@nemesifier

⚠️ Not suited to beginner contributors!

Is your feature request related to a problem? Please describe.

Yes, this is a security issue related to identity verification across different organizations.

The Security Problem:

Consider two organizations:

  • Org A: Requires identity verification for all users
  • Org B: Does not require identity verification

Current Behavior:

  1. User signs up to Org B (no identity verification required)
  2. User attempts to join Org A (which requires identity verification)
  3. The system allows the user to join Org A without completing identity verification
  4. This is a security vulnerability as users can bypass Org A's identity verification requirements

Why This Happens:

The current system assumes all organizations are managed by the same administrators who enforce consistent rules. However, this assumption doesn't hold when:

  • Different organizations have different security requirements
  • Organizations belong to different entities with varying compliance needs
  • Some organizations require strong identity verification (e.g., government, healthcare) while others don't

Why the Currently Proposed Alternative is Insufficient:

The patch in #674 introduces a cross_organization_login_enabled
setting that prevents users from joining organizations with different identity verification requirements.

While this prevents the security issue, it creates a new problem:

  • Users cannot join Org A at all if they already have an account in Org B
  • Users would need to create a new account with a different email/phone number, which may not always be feasible
  • This is not a practical solution for real-world scenarios

Describe the solution you'd like

Proposed Architecture: Per-Organization RegisteredUser Records

Core Concept:

Change the RegisteredUser model to support multiple records per user, with one record per organization. This allows each organization to have its own verification status, method, and requirements for the same user.

Technical Changes:

  1. Database Schema Changes:

    class AbstractRegisteredUser(models.Model):
        id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
        user = models.ForeignKey(
            settings.AUTH_USER_MODEL,
            on_delete=models.CASCADE,
            related_name="registered_users",
        )
        organization = models.ForeignKey(
            "openwisp_users.Organization",
            on_delete=models.CASCADE,
            related_name="registered_users",
            null=True,
            blank=True,
            help_text="The organization this registration info belongs to. "
                      "If null, applies to all orgs without specific requirements."
        )
        method = models.CharField(max_length=64)  # Registration method
        is_verified = models.BooleanField(default=False)  # Verification status
        modified = AutoLastModifiedField()
        
        class Meta:
            unique_together = ("user", "organization")
  2. Key Changes:

    • Change OneToOneField to ForeignKey for user relationship
    • Add organization field (nullable for backward compatibility)
    • Add unique_together constraint on (user, organization)
    • When organization=None, the record applies globally (backward compatible)
  3. API Changes:

    • When a user tries to join a new organization with different requirements:
      • Check if they have a RegisteredUser record for that org
      • If not, allow them to initiate the verification process
      • Create a new RegisteredUser record specific to that organization
    • Update IDVerificationHelper to check org-specific verification status:
      def is_identity_verified_strong(self, user, organization=None):
          try:
              # Try org-specific record first
              if organization:
                  reg_user = RegisteredUser.objects.get(
                      user=user, organization=organization
                  )
              else:
                  # Fall back to global record
                  reg_user = user.registered_users.get(organization=None)
              return reg_user.is_identity_verified_strong
          except RegisteredUser.DoesNotExist:
              return False
  4. Registration Flow:

    • When registering to a new organization:
      • Check if user already exists globally
      • If yes, check if a RegisteredUser record exists for this org
      • If not, initiate verification process (SMS, email, etc.)
      • Create org-specific RegisteredUser record upon completion
  5. Monitoring Integration Updates:

    • Update write_user_registration_metrics to handle multiple RegisteredUser records per user
    • Ensure metrics correctly count unique users per organization
    • Update post_save_radiusaccounting to get the correct registration method for the org
  6. Backward Compatibility:

    • Existing installations with organization=None continue working as before
    • New organizations can opt into per-org verification requirements
    • Global settings can define default behavior when organization=None

Benefits:

  1. Security: Users must complete identity verification for each organization that requires it
  2. Flexibility: Organizations can have independent onboarding rules (verification, limits, pricing)
  3. User Experience: Users don't need multiple accounts - they can use the same email across organizations
  4. Backward Compatibility: Existing single-org setups continue working without changes
  5. Extensibility: Supports future features like organization-specific pricing (openwisp-subscription)

Describe alternatives you've considered

Alternative 1: Current Branch Approach (Block Cross-Org Login)

Description: Use the cross_organization_login_enabled setting to prevent users from joining organizations with different requirements.

Problems:

  • Users cannot join organizations with different requirements at all
  • Forces users to create new accounts with different credentials
  • Not feasible for users who need access to multiple organizations
  • Poor user experience

Verdict: Rejected - too restrictive and impractical

Alternative 2: Global Identity Verification

Description: Require all users to complete the highest level of verification required by any organization they might join.

Problems:

  • Over-verification for users who only need low-security organizations
  • Privacy concerns - collecting more data than necessary
  • Increased friction for user registration
  • Not scalable across diverse organization types

Verdict: Rejected - too invasive and inflexible

Alternative 3: Organization-Specific User Accounts

Description: Create separate user accounts for each organization (like multi-tenant SaaS).

Problems:

  • Users must remember multiple sets of credentials
  • Difficult to manage account consolidation
  • Complex password reset flows
  • Poor user experience

Verdict: Rejected - duplicates data and creates management overhead

Alternative 4: Verification Requirements Inheritance

Description: Organizations can inherit verification from other organizations.

Problems:

  • Complex inheritance rules
  • Difficult to track which org verified the user
  • Potential for verification gaps
  • Harder to implement and maintain

Verdict: Rejected - adds unnecessary complexity

Additional context

Current Implementation Analysis

The current branch adds these files/changes:

  • New migration adding cross_organization_login_enabled field to OrganizationRadiusSettings
  • API changes in ObtainAuthTokenView.validate_membership() to check the new setting
  • Documentation updates explaining the setting
  • Tests for the new behavior

Files Modified in Current Branch:

  • .github/workflows/backport.yml (removed)
  • docs/images/organization_cross_login.png (new)
  • docs/user/rest-api.rst (documentation)
  • docs/user/settings.rst (documentation)
  • openwisp_radius/admin.py (admin UI)
  • openwisp_radius/api/views.py (API logic)
  • openwisp_radius/base/models.py (model changes)
  • openwisp_radius/migrations/0038_clean_fallbackfields.py (migration fix)
  • openwisp_radius/migrations/0043_organizationradiussettings_cross_organization_registration_enabled.py (new migration)
  • openwisp_radius/settings.py (new setting)
  • openwisp_radius/tests/test_api/test_rest_token.py (tests)
  • openwisp_radius/tests/test_api/test_utils.py (tests)
  • setup.py (dependencies)

Implementation Plan for Proposed Solution

  1. Phase 1: Database Schema

    • Modify AbstractRegisteredUser model
    • Create migration to:
      • Add id field as primary key
      • Change user from OneToOneField to ForeignKey
      • Add organization field
      • Add unique constraint on (user, organization)
      • Migrate existing data (set organization=None for all current records)
  2. Phase 2: API Updates

    • Update IDVerificationHelper.is_identity_verified_strong() to accept organization
    • Update ObtainAuthTokenView.validate_membership() to check org-specific verification
    • Update registration serializers to create org-specific records
    • Add helper method RegisteredUser.get_for_user_and_org()
  3. Phase 3: Monitoring Integration

    • Update write_user_registration_metrics() in monitoring/tasks.py
    • Update post_save_radiusaccounting() to get correct method for org
    • Ensure metrics handle multiple records per user correctly
  4. Phase 4: Admin Updates

    • Update RegisteredUserInline to show org-specific records
    • Update RegisteredUserFilter to filter by organization
    • Add organization column to user admin
  5. Phase 5: Testing

    • Unit tests for new model methods
    • Integration tests for cross-org registration flows
    • Tests for monitoring metrics with multi-org users
    • Backward compatibility tests
  6. Phase 6: Documentation

    • Update API documentation
    • Update admin documentation
    • Migration guide for existing installations
    • Best practices for multi-org setups

Use Cases Supported

  1. Single Organization (Current Behavior)

    • One RegisteredUser record with organization=None
    • All existing functionality unchanged
    • Zero migration friction
  2. Multiple Organizations, Same Requirements

    • One RegisteredUser record with organization=None
    • Applies to all organizations
    • Efficient and simple
  3. Multiple Organizations, Different Requirements

    • Global record for common orgs
    • Org-specific records for special orgs
    • User completes verification per org as needed
  4. Premium/Paid Organizations

    • Integration with openwisp-subscription
    • Org-specific registration metadata
    • Different pricing tiers per organization

Security Considerations

  1. Verification Isolation: Each organization's verification status is independent
  2. No Bypass: Users cannot use verification from Org B to access Org A
  3. Audit Trail: Each RegisteredUser record tracks when/where verification occurred
  4. Granular Control: Organizations control their own verification requirements
  5. Privacy: Users choose what data to share with each organization

Migration Path

For existing installations:

  1. All current RegisteredUser records will have organization=None
  2. Existing behavior continues unchanged
  3. Organizations can gradually adopt org-specific verification
  4. No breaking changes to existing APIs

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

Status

In progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions