The value the plugin stores (the site ID) will be predictable, so a cookie probably wouldn’t be a good method, since it could be forged; so I was thinking about storing it in a usermeta field instead. Then check it during an authenticate
callback, so that you could easily prevent the user from logging in if the check fails.
Now that I think about it a bit more, I don’t think we even need to store the site id; we just need the usermeta field to exist. It’s existence represents the fact that the user logged in with a 2FA token, so they should be allowed to access any sites that have 2FA enabled.
It does need to be cleared out whenever a user logs out manually or their auth cookie expires, though; otherwise the valid user would login the first time and it would be set, and then any time after that an attacker could login to a site without GA and still be able to access sites that do have GA.
So maybe it should be a transient with the user’s ID and the same expiration as the auth cookie, rather than a usermeta field.
Some scenarios:
Normal use case for valid user:
- User isn’t logged in
- User successfully logs into site with GA active
- GA creates the transient
- User visits admin page on different site that has GA active
- GA checks transient. It exists, so the user is allowed to continue.
- User visits admin page on different site that doesn’t have GA active
- GA doesn’t (can’t) do anything, they’re authenticated by their auth cookie
Attacker tries to bypass 2FA by logging into site where GA isn’t activated:
- Attacker isn’t logged in.
- Attacker logs into site without GA active
- The transient doesn’t get set, because GA isn’t running
- Attacker visits admin page on different site with GA active
- GA checks the transient. It doesn’t exist, so the callback returns false and the user is
auth_redirect()'d
to the login screen.
Here’s one scenario where it would break down: An attacker tries to bypass 2FA while valid user is logged in:
- Neither are logged in.
- Valid user logs in to site with GA active, transient is set.
- Attacker logs into site without GA, they get auth cookie
- Attacker visits admin page on site with GA active, they’re allowed because the transient exists
There’s probably a good way to fix that, though. Maybe generate a nonce and store it in a cookie that is set to expire when the auth cookie expires. At the same time, store that value in a usermeta field. When they try to visit an admin page, check if they have the cookie and if the value matches the one in their usermeta. That way GA could confirm that the user requesting the page is the same one that originally logged in with the token, and it would automatically expire at the right time.
There are probably some other edge cases that need to be considered, but I think that’s a good starting point.
(That got a bit lengthy and was kind of me thinking outloud. So, to summarize, I think the nonce-cookie-plus-usermeta method at the end would probably work best, rather than only a usermeta field, or only a transient.)