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.
- Apply the migration — the
pba_social_accountstable. The package applies it itself; whereexec()is forbidden, see the quick start. - Create an application at the provider and get the credentials.
- 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.
'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:
| Condition | What we do | |
|---|---|---|
| 1 | the provider account is already linked | let that user in |
| 2 | the person is already signed in on the site | link the network to their account |
| 3 | an e-mail arrived, the provider vouches for it, such a user exists | let them in and link the network |
| 4 | a verified e-mail arrived, no such user | create a new one and let them in |
| 5 | an e-mail arrived, but the provider never verified it, and an account with it exists | refuse |
| 6 | no 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
'social' => ['require_email' => true], // false — create accounts with no e-mail at allWithout 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:
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']),
);
}
}'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 siteWhen 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
- In the profile they open "Sign-in confirmation" and see the instructions and the key.
- 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.
- 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.
- 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
'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 nametwo_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").
| Button | What it does |
|---|---|
| View on the site | opens the user's public page. Signs nobody in |
| Sign in on the site | opens 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.
'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):
'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 isEmpty — 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.