Ok, I think I might have a solution 😬
-
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. -
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
-
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 newIS_AUTHENTICATED_PARTIALLY
would be added to figure out if you are authenticated, but have not reached your minimum level. We may also need aIS_AUTHENTICATED_2FA_IN_PROGRESS
or something, the different being that if you are "fully authenticated", then you havePARTIALLY
- but2FA_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 thatIS_AUTHENTICATED_*
for this. -
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.
-
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 😎
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 theacr
is clearly mentioned in this RFC.I may be wrong, but we could imagine a way where:
$token->getAuthenticationMethods(); // ['pwd', 'mfa']
).acr
-like infoacr_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)'
#[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