Since version 1.3.0: this subsystem is part of BricksMembers Access Control and is always registered, with runtime enforcement disabled by default until the Access Control Sessions setting is enabled.
This developer post documents the BricksMembers session limits and device management subsystem. The feature is an always-registered Access Control subsystem, not a ModuleRegistry module. Runtime enforcement is disabled by default through the Access Control Sessions setting.
The design goal is to protect login/session integrity without adding a broad runtime scan or a second session storage system. All enforcement is per-user, scoped to login and pending-resolution requests, and built on top of WordPress session tokens.
Ownership summary
| Concern | Owner |
|---|---|
| Settings persistence and sanitization | BaselMedia\BricksMembers\Access\Sessions\SessionManagementSettingsService |
| Effective limit resolution | BaselMedia\BricksMembers\Access\Sessions\SessionLimitPolicyService |
| Login enforcement and pending state | BaselMedia\BricksMembers\Access\Sessions\SessionManagementService |
| WordPress session token reads and precise non-current deletion | BaselMedia\BricksMembers\Access\Sessions\UserSessionTokenService |
| Known-device fingerprinting | BaselMedia\BricksMembers\Access\Sessions\SessionDeviceService |
| New-device email alert | BaselMedia\BricksMembers\Access\Sessions\SessionDeviceAlertService |
| Frontend render state and markup | BaselMedia\BricksMembers\Access\Sessions\SessionManagementRenderer |
| Frontend AJAX adapter | BaselMedia\BricksMembers\Access\Sessions\SessionManagementActions |
| Bricks element adapter | BRM_Element_Session_Management |
| Bricks structural style defaults | BaselMedia\BricksMembers\Elements\BricksElementStyleControlDefaults |
The canonical write owner is SessionManagementSettingsService. Admin pages, AJAX handlers, renderers, and elements are adapters. They must not create parallel mutation paths for the same settings or session-token behavior.
Bootstrap path
Access Control is loaded by the normal plugin Access path. AccessHookRegistrar::boot() calls SessionManagementRegistrar::boot(). The registrar claims its bootstrap through BootstrapState and registers the runtime hooks and AJAX actions.
attach_session_informationadds BRM login metadata to WordPress session payloads when the feature is enabled.wp_loginat priority30evaluates new-device alerts and limit enforcement after WordPress creates the login session.login_redirectat priority40redirects over-limit soft-gate users to the configured management page.template_redirectat priority3keeps pending users on the management page until they resolve the over-limit state.wp_ajax_brm_session_list,wp_ajax_brm_session_end, andwp_ajax_brm_session_continueexpose logged-in AJAX operations.
Session Management is intentionally not registered through ModuleRegistry. It protects account access and must be present even when optional modules are disabled.
Settings contract
The settings option is brm_session_management_settings. It is read and written only through SessionManagementSettingsService.
| Key | Type | Default | Notes |
|---|---|---|---|
enabled | bool | false | Master runtime switch. |
mode | string | soft_gate | Allowed values are soft_gate and auto_oldest. |
global_limit | int | 0 | Clamped to 0..100. 0 means unlimited. |
level_limits | array | [] | Map of level ID to positive limit. |
role_limits | array | [] | Map of role slug to positive limit. |
management_page_id | int | 0 | Must be a published page ID or 0. |
alert_new_device | bool | false | Enables the built-in new-device email alert. |
admin_bypass | bool | true | Users who can manage_options are not enforced when enabled. |
known_device_limit | int | 20 | Clamped to 1..100. |
is_enforcement_ready() returns false when the feature is disabled. In Soft gate mode, it also requires a published management page. Auto oldest can enforce without a management page because it does not need a frontend resolution screen.
Policy resolution
SessionLimitPolicyService::resolve_for_user( int $user_id ) returns an array with enabled, limit, and source.
- Disabled settings return
enabled=falseandsource=disabled. - Admin bypass returns
enabled=falseandsource=admin_bypass. - Global, role, and level candidates are collected.
- Limit
0is ignored. - The highest positive limit wins.
- No positive candidate returns
enabled=falseandsource=unlimited.
Role slugs come from the WP_User roles array. Active level IDs come from UserLevelAssignmentService::get_active_level_ids_for_user(), so session policy follows the same active-assignment semantics as the rest of Access Control.
Login enforcement flow
Disabled path
The hooks remain registered, but runtime methods return immediately after the cached settings check. This keeps the feature always available while making the default runtime cost a small per-request option read and early return.
Soft gate
- WordPress creates the login session.
SessionManagementService::handle_login()resolves the current or latest session verifier.- New-device alert logic runs if enabled.
- The effective policy is resolved under a per-user
ConcurrencyGuardServicelock. - If the user is over the limit,
mark_pending()stores a pending entry for the current verifier. filter_login_redirect()sends the user to the management page with the requested destination preserved inredirect_to.enforce_pending_session_resolution()keeps the user on the management page until the active session count is within the limit.continue_after_resolution()clears pending state and returns a validated redirect URL.
Auto oldest
Auto oldest calls UserSessionTokenService::destroy_oldest_sessions_until_within_limit(). The current verifier is never destroyed. If precise deletion fails and a management page exists, the flow falls back to pending soft-gate behavior.
Storage contract
| Storage | Owner | Purpose |
|---|---|---|
brm_session_management_settings option | SessionManagementSettingsService | Access Control session policy settings. |
session_tokens user meta | WordPress, with mutation isolated in UserSessionTokenService | Active WordPress login sessions. |
brm_session_resolution_pending user meta | SessionManagementService | Short-lived soft-gate pending entries keyed by verifier. |
brm_known_devices user meta | SessionDeviceService | Capped known-device fingerprints for login alert decisions. |
Pending entries expire after 15 minutes and are pruned on read. Known devices are capped by known_device_limit. No custom table is used in v1 because all required lookups are per-user and WordPress already stores sessions per user.
Token mutation boundary
UserSessionTokenService is the only BRM class that reads verifier-aware WordPress session token payloads or edits the WordPress session_tokens user meta directly. The public WP_Session_Tokens::get_instance( $user_id )->get_all() API returns session payloads without verifier keys, so verifier-aware listing reads the core user meta inside this boundary and falls back to get_all() only when a custom manager does not expose core meta. Precise deletion validates that the verifier exists, rejects attempts to delete the current session, removes only the requested non-current verifier, and returns WP_Error when precise deletion is unavailable.
Keeping direct token mutation in one service makes future adaptation easier for enterprise sites that use object cache, custom session token managers, or session-related security plugins.
Frontend surfaces
BRM_Element_Session_Managementregisters the Bricks elementbrm-session-management.SessionManagementRendererowns the shared render state and escaped markup.assets/js/session-management.jsowns the frontend AJAX interaction.BRM_Element_Session_Managementexposes shared button controls and visible content-tab styling groups for the screen, text, notices, session rows, badges/meta, action buttons, and messages.BricksElementStyleControlDefaultsowns generated-markup styling defaults as hidden Bricks controls forbrm-session-management.
The Bricks element does not enqueue base frontend CSS and there is no Gutenberg block or shortcode surface for this screen. It enqueues only the JavaScript runtime and localization needed for session actions. Visual styling comes from visible Bricks content-tab styling controls, shared BricksMembers button controls, classes, hidden structural defaults, and the site’s normal design system.
AJAX contract
| Action | Method | Behavior |
|---|---|---|
brm_session_list | POST | Returns current render state for the logged-in user. |
brm_session_end | POST | Ends a non-current verifier for the current user. |
brm_session_continue | POST | Returns a redirect URL only when the user is within the resolved limit. |
All AJAX actions use AjaxHandlers::verify_ajax_request( 'read', '', array( BRM_NONCE_FRONTEND ), 'POST' ) and require a logged-in user. brm_session_end rejects current-session deletion. Users must use the normal logout link for the device they are currently using.
Security and privacy
- Admin settings save requires
CAP_MANAGE_ACCESS_CONTROLand a dedicatedbrm_session_management_settingsnonce. - Frontend AJAX actions require the BRM frontend nonce and a logged-in user.
- Redirects are validated with WordPress redirect helpers before they are stored or returned.
- The current session is never destroyed automatically and cannot be ended through the AJAX endpoint.
- Known-device fingerprints are built from normalized browser family, platform family, and masked IP prefix.
- Raw full IP addresses are not stored in the fingerprint.
- Email delivery goes through
EmailTransportService, not a directwp_mail()call.
Performance model
The subsystem is intentionally per-user. It performs no global session scans and no broad user queries. The hot path is gated by a cached settings read. Enforcement runs on login or while a user has unresolved pending state.
- Settings are cached statically inside
SessionManagementSettingsService. - WordPress session token reads are scoped to one user.
- Known-device user meta is capped.
- Session limits are clamped to
100. - Per-user locks protect login enforcement and session termination from concurrent updates.
- User-agent handling is lightweight string matching, with no third-party parser dependency.
Extension points
brm_session_new_device_email_subjectfilters the new-device email subject.brm_session_new_device_email_bodyfilters the new-device email body.brm_session_new_device_loginfires after a new-device login is evaluated and remembered.
Extensions should not mutate session_tokens directly. Use UserSessionTokenService for BRM-owned session termination behavior, and keep external integrations behind hooks or future dedicated APIs.
Testing guidance
Unit coverage should focus on the policy resolver, settings sanitization, token service behavior, pending-state transitions, new-device alert decisions, and AJAX permission behavior. Browser coverage should verify the Access Control Sessions tab save path and the two-browser soft-gate flow.
Changes that touch this subsystem should at least run targeted PHPUnit coverage, PHPCS for changed PHP files, PHPStan, security QA, AI/MCP drift checks, and browser coverage for any admin or frontend behavior changes.
Edit guidance
- Start in
SessionManagementSettingsServicefor persistence, defaults, and sanitization. - Start in
SessionLimitPolicyServicefor role/level/global policy behavior. - Start in
SessionManagementServicefor login flow, pending redirects, and continue behavior. - Start in
UserSessionTokenServicefor active session reads and verifier-specific termination. - Start in
SessionManagementRendererfor frontend state or markup changes. - Start in
SessionManagementActionsfor AJAX validation and response handling. - Update
docs/feature-maps/access-control.mdwhenever ownership, hooks, storage, events, or surfaces change.