Authentication with Magic and Oauth2
- Getting started
- Development and installation
- Deployment for production
- Authentication and magic tokens
- Websockets for interactive communication
Minimum security requirements
The following is the baseline recommended approach for ensuring safe app authentication:
- Any user account change must require current password verification to ensure that it's the legitimate user.
- Login page and all subsequent authenticated pages must be exclusively accessed over TLS or other strong transport.
- Account recovery must ensure that the starting point is a logged-out account.
- Where a state is unclear, use two tokens (one emailed, one stored in the browser) with a handshaking / fingerprinting protocol to ensure a chain of custody.
- An application should respond with a generic error message regardless of whether:
- The user ID or password was incorrect.
- The account does not exist.
- The account is locked or disabled.
- Code should go through the same process, no matter what, allowing the application to return in approximately the same response time.
- In the words of George Orwell, "break any of these rules sooner than do anything outright barbarous".
On passwords:
- Use Argon2id with a minimum configuration of 15 MiB of memory, an iteration count of 2, and 1 degree of parallelism.
- Passwords shorter than 8 characters are considered to be weak (NIST SP800-63B).
- Maximum password length of 64 prevents long password Denial of Service attacks.
- Do not silently truncate passwords.
- Allow usage of all characters, including unicode and whitespace.
Authenticated email-based magic login
Most web applications permit account recovery through requesting a password reset via email. This is a weak point in the custodial chain even assuming a savvy user adhering to best-practice password conventions. In which case, secure this and offer it as a login option ... a magic login.
Any custodial changes to user-controlled information must be treated as requiring full authentication. Do not assume that a logged-in user is the authorised account holder.
- Ensure the user is logged out.
- User enters email in frontend and submits via API to backend.
- Check if an account exists, if not silently create one, and get the user id.
- Generate two 30-second-duration magic JWT tokens, each with a randomly-generated UUID sub (subject) and fingerprint, where sub1 != sub2 and fingerprint1 == fingerprint2.
- One is emailed to the user (with sub = user.id) and the other is returned to the frontend to be stored in the user's browser.
- Once the user clicks on (or pastes) the magic / emailed link and returns to the browser, check that the fingerprint in the stored and submitted tokens are the same, and submit both to the backend.
- Validate the tokens, and check compliance with sub and fingerprint rules.
- If the custodial chain is secure, return an access_token and refresh_token to the frontend.
- Note that the tokens provide no meaningful information to an adversary. No email address or personal information.
Users may not always have access to email, or use a password manager, making it easier (and faster) to login with a password. A fallback to a stored password must be offered. You could also choose to enforce passwords for admin users.
Account recovery and reset
Clearly a user does not need a password to login with this process, and so there is no enforcement in the core stack. However, an application may require evidence of a deliberate decision in the custodial chain. Enforcing a password is part of that chain, and that raises the need to reset a password if it is ever lost.
Password recovery functions much the same as the magic workflow, with the same dual token validation process, except that the user now goes to a page that permits them to save a new password.
The user can also change their password while logged in, but - mindful of the rules for validation - they will need to provide their original password before doing so.
Two-factor authentication
Time-based One-Time Password (TOTP) authentication extends the login process to include a challenge-response component where the user needs to enter a time-based token after their preferred login method.
This requires that the user:
- Install an authenticator app.
- Generate a QR code or key and pair that with their app.
- Confirm that they are paired.
After that, the user will be challenged to enter a 6-digit verification code to conclude each login.
The login workflow is extended as follows:
- TOTP requires the use of third-party token generators, and they seem to be stuck on sha-1 hashing. That means deliberately dumbing down from sha256.
- After successful login (oauth2 or magic) instead of generating access_token and refresh_token, instead generate a special access_token with totp = True as a key in the token.
- Specifically test for this in each authentication check. totp = True can only be used to verify a TOTP submission, not for any other purpose. The user is not considered to be authenticated.
- When the user submits their verification code, post that, plus the access_token with totp = True, to complete authentication and receive the standard access_token and refresh_token.
As before, enabling or disabling TOTP requires full authentication with a password.
Access and Refresh tokens
Persisting the authentication state of a user requires a mechanism to respond to an authentication challenge which does not inconvenience the user, while still maintaining security.
The standard method for doing so is via access_token and refresh_token, where:
- The access token is of short duration (30 minutes, or even less).
- The refresh token is of long duration (3 months, and sometimes indefinite).
- An access token can only be used to authenticate the user, and a refresh token can only be used to generate new access tokens.
- Access tokens are not stored, and refresh tokens are maintained in the database ... meaning they must be deliberately deactivated on use.
- When a user logs out, deactivate their refresh tokens.
Obviously, this still means that a long-living, active refresh_token is equivalent to authentication, which returns us to the caveat raised above:
Any custodial changes to user-controlled information must be treated as requiring full authentication. Do not assume that a logged-in user is the authorised account holder.