Skip to content

[Security] Split tokens in authentication-request and authenticated ones #30765

Closed
@wouterj

Description

@wouterj

This issue is a continuation of #21068 and a proposal for work during the FOSSA 2 EU hackathon.

Description

Main goal: Make Tokens first-class citizens in the security component, in favor of Tokens + Users. See https://wouterj.nl/2019/03/security-removing-user for some background.

In order to do this, I propose to first split the current tokens classes. This idea is already implemented in the Guard tokens.

Current state: all security providers have 1 token (i.e. UsernamePasswordToken) that represents both the input as the output of the authentication. I.e. $token->getUsername() returns either the username filled in by the user in the login form or $user->getUsername().
In this way, whether or not a token is authenticated is based on a boolean field (isAuthenticated) or the existence of roles in the token.

Splitting the tokens in an AuthenticationRequestToken and an AuthenticatedToken imho brings the following advantages:

  • It'll be possible to directly typehint for authenticated or un-authenticated tokens (no more if (!$token->isAuthenticated()) return). This also makes it clearer during which phase of authentication a specific class is called.
  • Tokens will no longer contain properties not used during that phase (i.e. $password should be empty after authentication)
  • "$user can be either a UserInterface, string or class implementing __toString()" rule. It'll be always a string during AuthenticationRequest (representing the user input) and it'll always be a user in AuthenticatedToken (representing the authenticated identity).
  • It removes magic parsing in Token::__construct() to fake JAVA constructor overloading.

Example

Before
Untitled Diagram (3)

After
Untitled Diagram (4)

Before

public function getUser()
{
    $token = $this->tokenStorage->getToken();
    if (null === $token) return;

    $user = $token->getUser();
    if (!$user instanceof UserInterface) return;

    return $user;
}

After

public function getUser()
{
    $token = $this->tokenStorage->getToken();
    if (!$token instanceof AuthenticatedTokenInterface) return;

    return $token->getUser();
}

Before

interface AuthenticationManagerInterface
{
    public function authenticate(TokenInterface $token): TokenInterface;
}

After

interface AuthenticationManagerInterface
{
    public function authenticate(AuthenticationRequestToken $token): AuthenticatedTokenInterface;
}

How to preserve backwards compatibility?

Of course, changing such a fundemental class in the Security system is quite a challenge to do BC. I propose to following UML diagram for tokens. As far as I can see, this would be backwards compatible for the token classes.

Untitled Diagram (5)

After then, we should look at how to make all methods and typehints backwards compatible. As no return typing is used, this must not be too hard.

Proposal of the steps

  1. Create AuthenticationRequestToken and AuthenticatedToken for anonymous and usernamepassword and deprecate the old token methods.
  2. Update the anonymous and usernamepassword authentication providers to use these new classes.
  3. Update the rest of the security system to use the new classes
  4. Look at the rememberme tokens, they are quite special and I haven't grasped yet how they work exactly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions