DEV Community

XerxesCross2735
XerxesCross2735

Posted on

Property Signup Links: SMS OTP Compliance Under GDPR, PSD2, NIST, and Phishing Risk

Decision rule: use an SMS verification link to prove reachability during ordinary property-management signup, but don't treat that proof as sufficient 2FA, a phishing-resistant login, or compliance by itself. GDPR asks whether the personal-data processing is appropriate and protected; PSD2 matters when the flow is actually strong customer authentication for a covered payment action; NIST treats authentication over the public switched telephone network as a restricted out-of-band method. Those are three different tests.

For a tenant or property manager creating an account, the low-integration path is a short-lived, single-use link sent through a narrow messaging adapter. Keep authorization out of the link. A backend issues an opaque token, stores only a digest, sends the URL, and consumes the token atomically. After that, the account's risk tier decides whether login needs a stronger authenticator.

SMS can be one layer. It can't carry the whole security model.

Threat-model the single-use verification link

The adapter should accept a destination and message, nothing more. That keeps provider-specific credentials and delivery responses outside the account domain, makes notebook tests cheap, and lets an eval harness exercise token behavior without sending messages. The account service owns token generation, expiry, single use, throttling, and the transition from pending to contact_verified; the messaging component owns submission and delivery telemetry. A delivery receipt can update operations data, but it must never mark the account verified.

Here is a compact Python example. It uses an in-memory store so the state transition is visible; production code needs a durable store with an atomic compare-and-delete operation. The ten-minute lifetime is an example policy value, not a standard-mandated duration.

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from hashlib import sha256
from secrets import token_urlsafe
from typing import Protocol


class MessageSender(Protocol):
    def send(self, phone_number: str, body: str) -> None: ...


@dataclass(frozen=True)
class PendingVerification:
    account_id: str
    token_digest: str
    expires_at: datetime


class VerificationService:
    def __init__(self, sender: MessageSender, base_url: str) -> None:
        self.sender = sender
        self.base_url = base_url.rstrip("/")
        self.pending: dict[str, PendingVerification] = {}

    def issue(self, account_id: str, phone_number: str) -> None:
        token = token_urlsafe(32)
        digest = sha256(token.encode("utf-8")).hexdigest()
        self.pending[digest] = PendingVerification(
            account_id=account_id,
            token_digest=digest,
            expires_at=datetime.now(timezone.utc) + timedelta(minutes=10),
        )
        link = f"{self.base_url}/verify-contact?token={token}"
        self.sender.send(phone_number, f"Verify your property account: {link}")

    def consume(self, token: str) -> str:
        digest = sha256(token.encode("utf-8")).hexdigest()
        record = self.pending.pop(digest, None)
        if record is None:
            raise ValueError("invalid_or_used_token")
        if datetime.now(timezone.utc) >= record.expires_at:
            raise ValueError("expired_token")
        return record.account_id
Enter fullscreen mode Exit fullscreen mode

Two details deserve more attention than the sender's SDK. First, logging middleware must redact the query string because the link is a bearer secret. That includes reverse-proxy access logs, analytics events, exception traces, and support tooling. Second, the production consume operation must be atomic. A read followed by a delete creates a race in which two requests can both succeed; a transaction or conditional delete gives one winner. This is the sort of case I put into an eval harness before wiring any external transport: two concurrent consumes, one expected success, one invalid_or_used_token, and no message delivery required.

Don't put the phone number, role, building identifier, or authorization claims inside the URL. An opaque token keeps the message terse and prevents the link itself from becoming a second database. It also makes deletion and revocation tractable because the server remains the source of truth.

The sender is downstream.

How should SMS OTP meet GDPR and PSD2 compliance?

Start by separating four questions that often get folded into the vague phrase “SMS compliance.” Is the phone number necessary personal data, and is its lifecycle controlled? Is the user performing a regulated electronic payment action? Does the selected authenticator meet the assurance target? What happens when an attacker controls the number or tricks the user into disclosing a code?

For GDPR, document the purpose for collecting the number, limit retention, restrict access, and define deletion and correction paths. Article 5 contains the purpose-limitation and data-minimization principles, while Article 32 frames security as measures appropriate to risk. Neither provision turns a delivered SMS into a compliance certificate. A property platform may have a sound reason to verify a contact number, but it still needs a lawful basis and a retention decision. The exact lawful basis can depend on the service and jurisdiction — I'm not sure a generic architecture article can settle that without the controller's facts and legal review.

PSD2 is narrower than “any login in Europe.” Its strong customer authentication definition calls for two or more elements categorized as knowledge, possession, and inherence, with independence designed so compromise of one doesn't compromise the others. The Regulatory Technical Standards also impose dynamic linking for electronic remote payment transactions: the authentication code must be linked to the amount and payee. A link proving control of a phone number during property-account signup doesn't establish those payment semantics. If the same product later authorizes rent or deposit payments, classify that action separately with the payment provider and counsel instead of reusing the signup verdict.

NIST SP 800-63B supplies the clearest engineering warning. Out-of-band authentication using the public switched telephone network is restricted, and verifiers are told to consider risk indicators such as device swap, SIM change, and number porting. The same guidance distinguishes phishing-resistant methods from manually entered out-of-band secrets. So an SMS OTP may provide useful friction at a lower-risk boundary, but it doesn't become resistant to a fake login page merely because the code expires quickly.

The practical answer is risk-tiered: keep SMS reachability verification for low-impact signup when the privacy analysis supports it; add a separate authenticator for persistent 2FA; and require a phishing-resistant option for administrators, payment-capable roles, or other high-impact actions. “Separate” matters. Sending a password and an SMS code through different screens doesn't help if account recovery collapses both factors back to the same phone number.

Three reviews. Three answers.

Integration boundary: keep login and recovery separate

SIM swapping breaks the assumption that the current holder of a telephone number is the original account owner. Phishing breaks a different assumption: a legitimate user can receive a valid OTP and still type it into an attacker-controlled page. Malware, lock-screen previews, recycled numbers, shared family plans, and support-assisted recovery add more routes around the intended factor. These aren't interchangeable failures, so one generic “SMS failed” metric won't tell an operations team what to fix.

A useful threat model maps each event to the property workflow. Consider one synthetic account, pm_1042, which begins as a prospective tenant and later receives permission to manage a building. At signup, the SMS link proves that somebody with current access to the number opened a fresh bearer URL; it does not prove a durable identity, so the account can save a basic preference but cannot see applicant records. Before the role change, the service requires a separate, phishing-resistant authenticator and records the factor-enrollment event. Now suppose an attacker gets the number through a SIM swap and requests recovery. The policy sees a recent factor change, refuses to let SMS replace the stronger authenticator by itself, notifies the previously verified channel, and revokes sessions only after the recovery ceremony succeeds. No single control solves the story. The useful result comes from keeping reachability, identity evidence, role authorization, step-up authentication, and recovery as separate state transitions, then testing every forbidden transition in the harness. Stick with SMS-only verification only when compromise has limited impact and another control protects later sensitive actions; it is not suitable as the sole factor for privileged access or payment authorization.

Recovery is the catch. Teams often harden the happy-path login, then let a support ticket replace every authenticator after the requester repeats data available in a breached profile. Model recovery as an authentication ceremony of its own: record factor changes, notify through an already verified channel, apply a risk-based delay where appropriate, and give users a way to revoke active sessions. A recent SIM change or number port can be a risk signal, as NIST notes, but the response should be policy-driven rather than an improvised block that strands legitimate users.

For messages sent in the United States, operational review should also cover the wireless ecosystem's messaging practices and consent expectations. CTIA publishes messaging interoperability principles and best practices, but carrier acceptance isn't the same thing as GDPR compliance, PSD2 strong customer authentication, or NIST assurance. Keep those evidence trails separate. It makes audits less theatrical and debugging much faster.

Threat-model delivery operations and test cost

Count integration effort across the full lifecycle, not the number of lines needed to send one message. The smallest credible slice includes secret management, destination normalization, consent and suppression state, token storage, atomic consumption, rate limits, delivery-status ingestion, redacted logs, dashboards, test doubles, and a sender-replacement boundary. The code above deliberately makes the transport replaceable because messaging policy and providers can change without forcing the authentication state machine to change.

Measure outcomes that expose both attacks and product friction: issue attempts per account and destination, token age at consumption, invalid or reused token counts, verification completion by channel, factor-change events, recovery starts, recovery completions, and privileged actions attempted without the required step-up. Use bounded labels; raw phone numbers, tokens, and full URLs don't belong in metrics. For traces, attach a generated correlation ID rather than the destination.

The test suite should be more demanding than the demo. Cover expiration boundaries, clock skew policy, duplicate delivery callbacks, reissue invalidation, concurrent consumption, throttling by account and destination, phone-number changes, session revocation after recovery, and log redaction. Then add abuse cases to the eval set — a burst against one number, a distributed burst against one account, and an old link opened after a newer link was issued. Prompt-cost awareness has an analogue here: every external send is a metered side effect, so deterministic local tests should prove the state machine before an end-to-end test spends quota or messages a real handset.

Keep the operational checklist in the release review itself. Before deployment, confirm that the data inventory names the phone number and delivery metadata, retention jobs match the documented policy, tokens never reach logs, consumption is atomic, resend behavior is explicit, rate limits cover both account and destination, delivery receipts cannot verify accounts, recovery doesn't collapse stronger factors into SMS, and high-impact roles have a phishing-resistant path. After deployment, review completion and abuse signals together; optimizing only completion can erase the very friction that limits takeover.

Choose assurance per property action

The clean decision isn't “SMS: yes or no.” It is a matrix of action impact, attacker capability, regulatory scope, recovery strength, and integration cost. Property signup can use an SMS link as a bounded contact-verification step. Routine login can require a distinct second factor. Portfolio administration, personal-data export, authenticator replacement, and payment actions can trigger stronger step-up rules.

This architecture keeps the compliance claims modest and the engineering useful. GDPR controls follow the data. PSD2 analysis follows the payment action. NIST assurance follows the authenticator and threat model. SMS remains available where its reach and low integration burden fit, while SIM-swap and phishing risk are handled by stronger factors and a recovery path that preserves them.

References

Top comments (0)