Skip to content

Social networks, 2FA, impersonation ​

Three subsystems, each enabled separately and each with its own pitfalls.

Social sign-in ​

Google, Yandex, Mail.ru, GitHub, Facebook and Telegram are supported. No libraries needed — the whole exchange is two requests to the provider.

Registration is the same thing as sign-in ​

There is no separate "register via a social network", and there is no need for one. The buttons sit in both the sign-in form and the registration form, they lead to the same place, and the first sign-in through a network creates the account.

Google, Yandex, GitHub and Mail.ru hand over a verified e-mail. If no such user exists yet, the account is created silently and the person is straight inside. No registration form, no password, no e-mail.

  • the username is taken from the name in the social network, Cyrillic is transliterated, a taken name gets a digit appended;
  • e-mail and avatar are carried over from the social network profile;
  • the password is set to a random value. The person does not know it and is not supposed to: they sign in through the network. If they want a password, they set one via "forgot password".

Telegram never hands over an e-mail, so an "enter your e-mail" page is shown, and after it the usual confirmation e-mail arrives.

What it takes to make this work ​

Three things. Until all three are done, there will be no buttons on the site.

  1. Apply the migration — the pba_social_accounts table. The package applies it itself; where exec() is forbidden, see the quick start.
  2. Create an application at the provider and get the credentials.
  3. Put the credentials into the config.

A provider without credentials is deliberately not shown

A button that is guaranteed to fail is worse than no button at all: the person clicks it, gets an error from the provider, and leaves, having decided the site is broken.

Connecting a provider ​

The redirect address in the application settings at the provider is https://your-site/auth/<provider>/callback, for example https://example.com/auth/google/callback.

php
'social' => [
    'providers' => [
        'google' => [
            'client_id'     => '…',
            'client_secret' => '…',
        ],
    ],
],

Keys: google, yandex, mailru, github, facebook, telegram. Telegram takes bot_name and bot_token from your bot instead of client_id/client_secret.

The button appears by itself: the social.buttons.tpl chunk is already wired into the sign-in and registration forms and shows only configured providers.

⚠️ How it is decided who gets let in ​

This is the most important place in the subsystem, and it is easy to poke holes here. The order is this:

ConditionWhat we do
1the provider account is already linkedlet that user in
2the person is already signed in on the sitelink the network to their account
3an e-mail arrived, the provider vouches for it, such a user existslet them in and link the network
4a verified e-mail arrived, no such usercreate a new one and let them in
5an e-mail arrived, but the provider never verified it, and an account with it existsrefuse
6no e-mail at all (Telegram)ask on a separate page

Row 5 is not nit-picking

If you let people in on an unverified e-mail, it is enough to create an account at such a provider with someone else's address to take over that person's profile on the site. That is why there is a refusal there, with a request to sign in with a password and link the network from the profile.

An account created from a hand-typed e-mail is activated only after confirmation by e-mail — otherwise a social network could be used to claim someone else's address.

If the provider gave no e-mail ​

php
'social' => ['require_email' => true],   // false — create accounts with no e-mail at all

Without an e-mail the person will not be able to recover access if the network falls away, so by default we ask.

Linking and unlinking in the profile ​

The social.accounts.tpl chunk (already wired into the profile form) shows a list of networks: linked ones with an "unlink" button, the rest with "link".

The last network of a user created through a social network cannot be unlinked: they do not know a password of their own and would be left with no way in at all. As soon as they set a password — by changing or recovering it — the restriction lifts by itself.

Your own provider ​

A class satisfying the SocialDriver interface; for ordinary OAuth2 it is easier to extend AbstractDriver and describe four addresses:

php
class MyDriver extends \Boshnik\PbAuth\Social\AbstractDriver
{
    public static function key(): string { return 'mysite'; }
    protected function authUrl(): string  { return 'https://mysite/oauth/authorize'; }
    protected function tokenUrl(): string { return 'https://mysite/oauth/token'; }
    protected function userUrl(): string  { return 'https://mysite/api/me'; }

    protected function mapUser(array $response, array $token): \Boshnik\PbAuth\Social\SocialUser
    {
        return new \Boshnik\PbAuth\Social\SocialUser(
            id: (string) $response['id'],
            email: (string) $response['email'],
            emailVerified: !empty($response['email_confirmed']),
        );
    }
}
php
'social' => [
    'drivers'   => ['mysite' => \PageBlocks\App\Social\MyDriver::class],
    'providers' => ['mysite' => ['client_id' => '…', 'client_secret' => '…']],
],

Fill in emailVerified honestly

It is exactly what decides whether an existing account is entered on a matching e-mail — row 3 of the table above. Setting it to true where the provider does not verify the e-mail means you open up takeover of other people's profiles with your own hands.

Sign-in confirmation by code (2FA) ​

On top of the password, sign-in can also ask for a six-digit code from an app on the phone. A stolen password is then not enough to get in.

This is enabled by each user for themselves in the profile, in the "Sign-in confirmation" section. Nothing needs configuring — the section appears by itself.

The code is not sent anywhere ​

This is the first thing that raises a question. The site sends nothing — no SMS, no e-mail, no push. No delivery service has to be connected.

The code is shown by an authenticator app that the user installs on their phone themselves: Google Authenticator, Microsoft Authenticator, Authy, 1Password, Bitwarden, Aegis, FreeOTP — any of them, they are interchangeable. The site has no relationship with them: no contract, no keys, no payment.

       secret key — one and the same
              ↓                 ↓
    phone app                  the site
    key + current time         key + current time
         428 913                  428 913
              ↓
    the person enters the code on the site

When enabling, the site shows the secret key and the person enters it into the app. From then on, every 30 seconds both sides independently compute a six-digit number from the key and the current time. The code is not transmitted in either direction — there is nowhere to intercept it.

Why not by e-mail or SMS ​

  • SMS costs money and requires a contract with a gateway. Here it is zero.
  • Delivery may not happen: an e-mail goes to spam, an SMS is delayed. Here there is no delivery at all.
  • Works offline — the app shows the code even in airplane mode.
  • E-mail is unfit as a second factor in principle: if the person's e-mail password was stolen, the e-mail with the code arrives in the same place, and there is no protection.

What it looks like for the user ​

  1. In the profile they open "Sign-in confirmation" and see the instructions and the key.
  2. From a phone they tap the link — the app opens and adds the account. From a computer they type the key by hand, it is shown in groups of four characters.
  3. They enter the code the app showed. Until the code is entered, nothing is enabled: otherwise you could lock yourself out by saving a key that never made it into the app.
  4. They get backup codes and save them.

Backup codes ​

Eight one-time codes in case the phone is lost — without them a lost phone would mean a lost account. They are shown exactly once, when enabling; the database holds only hashes, they cannot be recovered. A used code is struck off.

They can be reissued in the same place in the profile, confirmed with the current password. The old ones stop working immediately.

Settings ​

php
'two_factor_enabled'       => true,  // master switch
'two_factor_window'        => 1,     // how many adjacent 30-second intervals to accept
'two_factor_challenge_ttl' => 300,   // seconds between the password and the code
'two_factor_attempts'      => 5,     // wrong codes before reset; 0 — no limit
'two_factor_backup_codes'  => 8,     // how many backup codes to issue
'two_factor_issuer'        => '',    // whose name the app shows; empty — the site name

two_factor_window exists because the clocks on the phone and on the server are always a little out of sync. 1 means ±30 seconds, usually enough. More than that weakens the protection: the code lives longer.

two_factor_enabled => false is an escape hatch

It drops the code requirement at sign-in and removes the section from the profile, but it does not erase the saved keys. If something went wrong and people cannot sign in, the site opens up with one line in the config, and after the fix everything comes back as it was.

What happens inside ​

Before the code is entered the user is not signed in. The session holds only their id and the expiry of the pending state — no password, no open context session. The session is opened only after a correct code.

The same code is not accepted a second time: the interval it matched is remembered. Otherwise a code glimpsed over the shoulder would keep working for another half a minute.

The secret is stored in the profile's extended under the pbauth key — the component needed no table of its own, and the fields the site puts there are left untouched.

If the user lost both the phone and the codes ​

From the interface, no way — that is the whole point of a second factor. Only an administrator can remove it: Manage → Users, and for the user in question clear the pbauth key in the Extended Fields (extended) field.

There is no QR code ​

Deliberately. Drawing a QR means either a third-party library that cannot be installed on an SFTP-only server (composer does not run there), or an encoder of our own that there is nowhere to test before release. So instead of a picture:

  • an otpauth:// link — on a phone it opens the app directly, which is even faster than scanning;
  • the key in groups of four characters — typed by hand into any app.

Need a QR — draw it on your side: in the form.twoFactor.tpl chunk the link carries a data-pbauth-totp-uri attribute, hook up any JS library and render its contents.

Do not send this value to a third-party QR generation service

It contains the secret, and you would be handing it over together with the second factor.

Per-user buttons in the manager ​

pbAuth adds two buttons to a user. Both in two places: Manage → Users (right-click on the row) and the user edit page (next to "Save").

ButtonWhat it does
View on the siteopens the user's public page. Signs nobody in
Sign in on the siteopens the site as this user

The difference is fundamental: the first one just shows a page, the second replaces your session on the site with the user's one.

Sign in on the site ​

Needed when you have to see the site through the eyes of a specific person — to work out why something is not displaying for them. Works right away, nothing to configure.

Who can use it. Only a manager with the sudo flag. An ordinary manager gets "Insufficient permissions", even if they type the address by hand. The check goes by the current manager session: the manager and the site share one session, so it cannot be forged without signing into the panel.

Blocked and unactivated accounts will not open — the user themselves cannot sign in under them either.

How to get back out. With the ordinary "Sign out" on the site. The manager session survives this — the panel stays open in its own tab.

php
'impersonate_enabled' => false,          // the address answers 404, the button disappears
'redirects' => ['impersonate' => '/profile'],

To record the fact of an impersonation (for audit — who signed in as whom):

php
'listeners' => [
    Dispatcher::AFTER_IMPERSONATE => [LogImpersonation::class],
],

$params will carry user (who was signed in as) and manager (who signed in).

View on the site ​

Simply opens the user's public page. The address is set by the pbauth_user_page setting in System → System Settings:

users/{id}                   → https://site/users/123
profile/{username}           → https://site/profile/vasya
https://other.site/u/{id}       an absolute address is used as is

Empty — no button. The default is users/{id}; if there is no such page on the site, clear the setting — otherwise the button leads to a 404.

There are no buttons in the manager at all

Check that System → Plugins has the pbAuth plugin and that the OnManagerPageBeforeRender event is attached to it. The installer attaches it itself; by hand this is only needed if the component was laid out as files, bypassing the package.

pbAuth — login, registration and profile for PageBlocks