Skip to content

Instantly share code, notes, and snippets.

@weaverryan
Last active March 26, 2023 17:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save weaverryan/d828ef860b1119862cf3828d016b8f41 to your computer and use it in GitHub Desktop.
Save weaverryan/d828ef860b1119862cf3828d016b8f41 to your computer and use it in GitHub Desktop.
MFA/2FA Symfony Notes

Ok, I think I might have a solution 😬

  1. On the token object, we introduce a concept of "trust level" - e.g. public function getTrustLevel(): int. It would be an integer level. This level would be set by an authenticator via a badge. The authenticator may set the integer level directly - e.g. 2fa auth sets a level of 10 (or something), or it may set it via some alias (e.g. 2fa ) and then the user is able to map which authenticators get which level. Small detail tbd. Related: an authenticator would somehow need to mark themselves as MFA-aware. The result would be that the existing token would not be replaced, but would be "enhanced" with the new trust level and maybe some sort of badge/stamp that records all of the authentication methods. We are also considering that a MFA authenticator may have a different interface - e.g. authenticate(UserInterface $user, Request $request) and returns some sort of object that communicates trust level... or anything else we think might need to be communicated. The $user argument would be the currently-authenticated user.

  2. Introduce the concept of "trust level" on authentication entrypoints - e.g. public function getTrustLevel(): int. Then, extend the entrypoint system to be smarter. For example, if access is denied because some URL requires trust level 8 and the user only has trust level 4, try to find an entrypoint that gives you 8 or higher. If one is found, use that entrypoint instead of a 403. This will allow each "mfa" to have its own entrypoint to initialize it. There will need to be some discovery about how - when we deny access in a controller, access_control, etc - how we communicate which "trust level" we want. Small detail tbd

  3. Introduce a new voter that's all about trust level - e.g. isGranted('TRUST_LEVEL', 6) or maybe isGranted('TRUST_LEVEL', '2fa') where the string is probably the name of the authentication system. Need to think about that 😛. Most people wouldn't use this anyways, it's only for the case where you want to require 2fa for part of your site only. Related: we should make IS_AUTHENTICATED_FULLY and remembered only return true when you have your minimum access level. A new IS_AUTHENTICATED_PARTIALLY would be added to figure out if you are authenticated, but have not reached your minimum level. We may also need a IS_AUTHENTICATED_2FA_IN_PROGRESS or something, the different being that if you are "fully authenticated", then you have PARTIALLY - but 2FA_IN_PROGRESS is a flag that you only get when you are literally stuck in this space. Or maybe we come up with a totally different thing that IS_AUTHENTICATED_* for this.

  4. We do not need the ability to have a token that is "not authenticated". Instead, we would have some listener on each request that detects if a user doesn't have their minimum level and redirects to an appropriate entrypoint.

  5. Add an optional new interface to the User - e.g. RequiredTrustLevelInterface with getMinimumTrustLevel(): int. This would allow some (or all) users to require MFA. For example, if I return 4 (?) from this (maybe we decide normal authentication is trust level 0, and 2fa is 4... maybe we use constants? tbd), then during 1st FA authentication, some listener on the authenticator system automatically detects that my level is not enough and automatically use the entrypoint system (to guarantee a good UX) to find the entrypoint that has at least a trust factor of 4, and it would use it (e.g. redirect). Of course, the user could try to navigate away from this page, but if they tried to access any other page, they are not authenticated and system (2 & 4) would cause me to get redirected right back to the 2fa entrypoint.

WDYT? I think it works... and it feels like we're mostly adding things, not trying to change interfaces, which would make this more doable 😎

@Spomky
Copy link

Spomky commented Mar 26, 2023

Hi,

When I was playing with the OpenID Connect specification, I noted something similar in the Core spec:

In the section 2 ID Token, the claim acr (Authentication Context Class Reference) seems to correspond to what is described in the first point.
Also, the amr (Authentication Methods References) claim can be linked to that purpose. Most AMR values are part of the RFC8176 and referenced in the IANA registry. The relationship with the acr is clearly mentioned in this RFC.

I may be wrong, but we could imagine a way where:

  • an equivalent of the authentication methods are populated during the authentication process depending on the required badges. These methods could be retrieved using the Token class (e.g. $token->getAuthenticationMethods(); // ['pwd', 'mfa']).
  • routes could be protected by an equivalent of the authentication context/requirements e.g. with expression language or defined aliases:
    • Examples of acr-like info
      • acr_level_0 =>'rmc OR pwd' (rmc stands for remember me cookie)
      • acr_level_1 =>'pwd AND mfa'
      • acr_level_2 =>'user AND fpt'
      • acr_level_3 =>'x509 OR (hwk AND pin)'
    • Examples of route protection
      • #[Route(..., acr: 'acr_level_0')]
      • #[Route(..., acr: 'acr_level_2 OR acr_level_3')]

The re-use of RFC or already registered concept could be useful, especially when symfony/symfony#48272 will be available

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment