Skip to main content

Pluggable Auth and Sovereign Data Handover

· 3 min read
Revant Nandgaonkar
Maintainer of Framework M
Pratham Rupasar
Developer at Framework M

Enterprise applications often face conflicting identity requirements. On one hand, security architects demand strict single sign-on (SSO), multi-factor authentication (MFA), and centralized session revocation via OpenID Connect (OIDC). On the other hand, operational teams require passwordless mobile app login flows, SMS gateway integrations, and deterministic data ownership handover when users leave an organization.

Framework M reconciles these needs with Pluggable Identity Self-Service, OIDC RP-Initiated Logout, core Domain Events, and Controller Ownership Transfer Hooks.


1. Passwordless Mobile Login & Identity Lockout Protection

In multi-tenant SaaS and enterprise mobile environments, requiring passwords for mobile users creates unnecessary friction and password-reset overhead.

Framework M introduces isolated signup and login forms supporting passwordless mobile authentication strategies:

Auth Self-Service Sequence

Key Capabilities:

  • Passwordless Mobile App Login: Mobile users authenticate seamlessly via phone number and OTP—enter mobile number, receive OTP via SMS, and log in directly with zero passwords required.
  • Pluggable SMS Gateway Protocols: Wire custom carriers or SMS providers (Twilio, MessageBird, SMPP) via SMSGatewayProtocol for mobile OTP sign-in and phone verification flows.
  • Sign-Up to Log-In Fusion: Automatic session promotion following self-service OTP verification without requiring redundant re-authentication steps.

2. Standard Domain Events (framework_m_core.events)

Framework M defines strongly typed domain event classes in framework_m_core.events that are published to the EventBusProtocol:

User Authentication Events

  • UserLoggedIn (user.logged_in): Emitted when a user completes authentication (attributes: user_id, auth_method such as phone_otp, credentials, or oidc, and optional email if assigned).
  • UserLoggedOut (user.logged_out): Emitted when a user session is revoked or logged out (attributes: user_id, optional email).
  • UserEmailUpdated & UserPhoneUpdated: Emitted when user contact identifiers change.

Subscribing to Auth Events

from framework_m_core.di import inject
from framework_m_core.interfaces.bootstrap import BootstrapProtocol
from framework_m_core.events import UserLoggedIn, UserLoggedOut


class SecurityAuditSubscriber:
async def handle_login(self, event: UserLoggedIn) -> None:
identifier = event.email or event.user_id
logger.info(f"User {identifier} logged in via {event.auth_method}")

async def handle_logout(self, event: UserLoggedOut) -> None:
logger.info(f"User {event.user_id} session logged out")


class RegisterAuthListeners(BootstrapProtocol):
order = 50

@inject
def run(self, container: Container) -> None:
event_bus = container.event_bus()
subscriber = SecurityAuditSubscriber()
event_bus.subscribe("user.logged_in", subscriber.handle_login)
event_bus.subscribe("user.logged_out", subscriber.handle_logout)

3. Controller Ownership Transfer Hooks (before_transfer_ownership & after_transfer_ownership)

When an employee changes roles or leaves an organization, their assigned documents, tasks, and system ownership must be transferred safely without dropping active workloads or violating compliance mandates.

Framework M fires explicit BaseController lifecycle hooks on affected DocType controllers during ownership transfer operations:

from framework_m_core.domain.base_controller import BaseController


class SupportTicketController(BaseController):
async def before_transfer_ownership(self, context: dict | None = None) -> None:
"""Hook called before transferring document ownership to a new user."""
new_owner_id = context.get("new_owner_id") if context else None
logger.info(f"Ticket {self.doc.name} transferring to {new_owner_id}")

async def after_transfer_ownership(self, context: dict | None = None) -> None:
"""Hook called after successfully transferring document ownership."""
old_owner_id = context.get("old_owner_id") if context else None
logger.info(f"Ticket {self.doc.name} transferred from {old_owner_id}")

Executing UserManager.transfer_ownership() triggers these hooks on every affected DocType controller while preserving immutable audit trails (ActivityLog / ErrorLog).


Learn More

Check out the full guides in our documentation: