Authentication and sessions

The Member module owns accounts but leaves sign-in open. Each way to sign in is a submodule that registers itself as a method, the session carries the logged-in member from one request to the next, and a page that needs a member throws an error the site catches to redirect to login.

Sign-in methods as submodules

The Member module does not hard-code how members sign in. Instead it defines a small surface that a sign-in method registers against, and each method is its own submodule. Two ship with the framework: MemberAuthMethodEmail, which signs a member in by email, and MemberAuthMethodMobile, which signs them in by mobile number. A site includes the methods it wants; the ones it includes are the ones the login and register pages offer. Because each method is a separate module, adding sign-in by another channel is a matter of writing a new method module, not changing the Member module.

How a method registers

A method registers itself with the Member module by declaring the pieces the member pages need from it: the form fields shown on the login and register pages, and the callback that sends a member their one-time code. The Member module holds the account, the code lifecycle and the pages; the method supplies the channel-specific parts. This is loose coupling in practice, the same pattern the framework uses everywhere: the host module declares an extension point and the submodule fills it, so neither has to know the other's internals. See Loose coupling and extension and Submodules, providers and vendors.

Codes go out through Messaging

Both methods work by sending a short code to prove the member controls the address or number they signed in with. Neither method talks to an email or SMS provider directly; each hands the finished message to the Messaging module and lets it deliver. The email method builds a subject and body from its configured templates and sends through MessagingEmail::Send; the mobile method fills its code into a message template and sends through the SMS side of Messaging. The configuration each method needs, its from-identity and its message templates, is provided the same way as any other module's settings:

Config::Provide('MemberAuthMethodEmail', [
    'MEMBER_AUTH_METHOD_EMAIL_LOGIN_SUBJECT' => 'MySite::LoginSubject',
    'MEMBER_AUTH_METHOD_EMAIL_LOGIN_TEXT'    => 'MySite::LoginText',
    'MEMBER_AUTH_METHOD_EMAIL_LOGIN_HTML'    => 'MySite::LoginHtml',
    // ... FORGOT subject / text / html ...
    'MEMBER_AUTH_METHOD_EMAIL_FROM_NAME'  => function(MemberCodeData $codeData) {
        return Config::Get('Messaging', 'MESSAGING_EMAIL_FROM_NAME');
    },
    'MEMBER_AUTH_METHOD_EMAIL_FROM_EMAIL' => function(MemberCodeData $codeData) {
        return Config::Get('Messaging', 'MESSAGING_EMAIL_FROM_EMAIL');
    },
]);

The template values are callables invoked at send time, one per message the method sends (a login code and a forgotten-password code, each with a subject, a text body and an HTML body). They receive a MemberCodeData describing the code being sent, so a site can tailor the wording per member if it wants.

Sessions carry the logged-in member

Once a member has signed in, the Member module records that in the session and reads it back on every later request to resolve the logged-in viewer. The Session module is the plumbing underneath: it is a thin, typed wrapper over the PHP session that starts it and offers a small set of read and write calls. It owns no tables and declares no settings; it just gives the rest of the site a tidy place to keep per-visitor state. A companion module, SessionTracking, hooks in to record the current session id for request tracking; it is a hook rather than an API you call.

Not-logged-in handling

A member-only page does not litter itself with checks for a signed-out visitor. It asks for the member, and when nobody is logged in the Member module throws MemberNotLoggedInError. The site catches that one error around its render pass and redirects to the login page, built by name with MemberUrl::Login:

try {
    Site::Route();
    Member::Route();
    Admin::Route('/admin');
}
catch (MemberNotLoggedInError $ex) {
    Ui::OverrideRedirect(MemberUrl::Login());
}

One catch covers every protected page. A member-only route never handles the signed-out case itself; it throws, and the site turns that into a redirect. This is the errors-and-happy-path idea from the Errors and the happy path standard: throw when a precondition is not met rather than thread null checks through the code that assumes it is.

Next: Identity and verification.