HV Tech Stack Chapter 04 · Auth

Auth.

How residents log in. How staff log in. How the old roster moves over. Who sees what.

Contents

In this chapter

  1. i.Magic-link authenticationI
  2. ii.Provisioning and unit bindingII
  3. iii.Staff authenticationIII
  4. iv.Who sees whatIV

I.

Part One

Magic-link authentication

Residents skew 55 and older. Password fatigue is the top ticket at peer HOAs. The right tool is a passwordless magic link.

The flow

  1. Resident lands on /account or hits a gated URL.
  2. They enter their email.
  3. WordPress checks the email against wp_users. The on-screen response is identical whether the email matches or not: “If that address is on file, we sent a link.” The form never reveals which emails belong to residents.
  4. If matched, WordPress generates a one-time token, stores the hash, and sends the link through SendGrid or Mailgun.
  5. Resident clicks the link. It opens a landing page with a single “Log me in” button. The page load does not consume the token. The button click (a POST request) does. WordPress validates the token, starts the session, and redirects to the destination.
  6. The redirect accepts only same-site paths. A full URL in the request gets ignored and replaced with /residents/.
  7. Token expires after 15 minutes or first use.

Plugin choice

The right plugin is Passwordless Login by Cozmoslabs (free) or Magic Login Pro (freemium). Both ship a clean magic-link flow. Both pair with ACF, BuddyPress, and WP Mail SMTP.

Mail delivery

Magic links fail silently if mail lands in spam. The system is not optional.

Throttling

ACF magic_link_last_sent on every user. WordPress blocks a second link within 60 seconds. Prevents flood attacks on a known address.

Hardening decisions (added 2026-07-05)

Four decisions from the auth security review. The first three bake into the build; the fourth is a policy call.

  1. Confirm-button landing page. Mail providers and antivirus tools pre-click every URL in an email to scan it. If the page load consumed the single-use token, the scanner would burn the link before the resident saw it, or in the worst case log the scanner in. So the emailed link opens a landing page (a GET) that changes nothing. Only the “Log me in” button click (a POST) consumes the token and starts the session. Verify the chosen plugin behaves this way; if it consumes tokens on GET, wrap or replace that step.
  2. No account enumeration. Whatever the resident types into the login form, the response reads the same: “If that address is on file, we sent a link.” Different responses for known and unknown emails would let anyone probe which addresses belong to residents.
  3. Redirect allow-list. The post-login redirect accepts only same-site paths. A crafted link carrying an off-site destination must fail closed to /residents/. This blocks phishing pages that piggyback on a real login.
  4. Shared inboxes are a policy question, not a code question. Many couples share one email address, so one inbox can control two resident records. Decision D11 on the operations page holds the recommended default. Pick before migration, because account provisioning depends on it.

II.

Part Two

Provisioning and unit binding

Import from HOA-sites. Bind each resident to their units. Every logged-in resident sees only their own data.

HOA-sites export

The current platform holds the master list. The plan assumes CSV export. Confirm this with the HOA-sites vendor before the migration phase. The exact export format drives the importer.

Target shape for the importer:

ColumnMaps to
emailwp_users.user_email, hv_residents.login_email
first_namehv_residents.first_name
last_namehv_residents.last_name
unit_numberhv_units.unit_number via join
roleOwner, Co-owner, Tenant

Import tool

WP All Import handles the CSV-to-WordPress step. A Power Automate flow picks up the imported rows and creates matching Dataverse hv_residents rows.

  1. Import units into hv_units from the condo association roster.
  2. Import residents into hv_residents.
  3. Build hv_resident_units join rows from the CSV's unit references.
  4. Let HV-DV-Sync-ResidentRoster provision the WordPress users.
  5. Send a one-time "your account is ready, click to log in" email. First click is the first magic link.

New resident requests

No open registration. The /account/request form creates hv_access_requests. Office staff verifies against the community roster. On approval, Power Automate creates the hv_residents record. The downstream flow provisions the WordPress user.

Unit binding

Single-unit owner
wp_usermeta.dataverse_resident_id maps to hv_residents.resident_id. ACF associated_units holds one row. Work-order proxy, variance dashboard, and vehicle list all filter by that one unit.
Multi-unit owner
ACF associated_units holds N rows, one per unit. The proxy queries MaintainX for the array and merges results. Variance dashboard shows a unit selector. Forms default to the primary unit but allow a dropdown.
Tenant vs owner
hv_resident_units.role drives permissions. Tenants see work orders for their unit. They do not see variance history or vehicle records. ACF can_submit_variance checkbox on each row governs the front-end.

Contact info changes

The Request Pattern governs every change. Residents cannot mutate wp_users.user_email directly.

III.

Part Three

Staff authentication

Staff uses Entra ID. Three tiers. Four WordPress roles.

Three tiers

Builder tier (Premium)
Users: Nate, the sole builder. Access: Power Apps, Dataverse schema, Power Automate flow library, WordPress admin. License: Power Apps Premium plus Dataverse ($20/month, one seat). Add a second seat only when a second builder exists.
Approver tier (Standard)
Users: Carol, Sarah, other liaisons. Access: Teams Adaptive Cards for approvals; WordPress admin scoped to their hub. License: standard M365.
Read-only tier (Standard)
Users: broader staff, library volunteers. Access: Excel files on SharePoint; selected WordPress admin for their content niche. License: standard M365.

Automation seats (Power Automate Premium)

Two standalone seats at $15/user/month each. Power Automate Premium is not bundled with Power Apps; Microsoft's docs are explicit. Dataverse is a premium connector, so every flow that writes to it needs one of these seats.

WordPress roles

RolePermissions
ResidentRead gated pages; submit forms; edit own profile within the Request Pattern
Content EditorEdit announcements, clubs, facilities, meetings for their hub
Data ManagerFull admin; Nate's role
PendingCan log in only to see the "account awaiting approval" page

Gated CPTs enforce role checks in template_redirect. Public CPTs render without auth.

Approvals without Power licenses

The cards-only flow is intentional. Carol and Sarah never open Power Apps. The service account holds the premium license and posts on their behalf. Microsoft's license model allows this for occasional use by a human approver.

Session and cookie rules

IV.

Part Four

Who sees what

Directory privacy, hard cases, and the full permission matrix on one page.

Directory privacy

The resident directory is opt-in per field. Default is everything hidden.

Staff see everything. Residents see only opted-in rows. The ACF directory_opt_in field cluster drives this.

Lost accounts and hard cases

Permission matrix

Surface Public Pending Resident Content Editor Data Manager
/yesyesyesyesyes
/buyers/yesyesyesyesyes
/residents/noawaiting approvalyesyesyes
/governance/mostly publicyesyesyesyes
/living-here/yesyesyesyesyes
/contact/yesyesyesyesyes
/wp-admin/nononoscopedfull
Power Appsnonononoyes (tier 1)
Teams cardsnononoyesyes
Excel on SharePointnononoreadread/write

Out of scope