$30 off During Our Annual Pro Sale. View Details »

Modern Security with Symfony's Shiny new Security Component

weaverryan
December 04, 2020

Modern Security with Symfony's Shiny new Security Component

Yes, Symfony's Security component is powerful. But... it's *also* complex. Can we have both? Power and flexibility with readable and expressive code?

We think so. That's why, in Symfony 5.1 & 5.2, the security component was rebuilt and *reimagined*. Complexity was stripped away, logic was centralized and intelligent hook points were added.

The result is a security system that can *do* more with code that you can understand.

In this talk, I'll introduce you to the new security component and show you how to activate and migrate to it (hint: it's simple!). We'll also dive into some of the new possibilities, like streamlined custom authenticators (bye Guard!), centralized "login throttling", magic login links, 2fa and more!

And since no security talk would be complete without API authentication, I'll give you a quick guide to how *you* should implement API auth and how that fits into the new system.

Let's go!

weaverryan

December 04, 2020
Tweet

More Decks by weaverryan

Other Decks in Technology

Transcript

  1. Modern Security
    with Symfony's Shiny
    new Security Component
    by your friend Ryan Weaver
    @weaverryan

    View Slide

  2. > Member of the Symfony docs team
    > Some Dude at
    SymfonyCasts.com
    > Husband of the talented and
    beloved @leannapelham
    symfonycasts.com
    twitter.com/weaverryan
    Yo! I’m Ryan!
    > Father to my much more
    charming son, Beckett

    View Slide

  3. Security System:
    A History
    Part 1
    @weaverryan

    View Slide

  4. 2010
    Today



    View Slide

  5. @weaverryan
    What Happened?

    View Slide

  6. Massive, massive complexity
    Original implementation based off of Java Spring
    Powerful
    Complex
    … almost no-one understood how it worked
    @weaverryan

    View Slide

  7. GET /foo
    X-Token: abc123
    Firewall
    Auth
    Listeners
    Unauth'ed Token
    abc123
    Authentication
    Manager
    Auth
    Providers
    Auth'ed
    token
    Set onto security context
    Do many other things
    @weaverryan

    View Slide

  8. The Simple form_login
    UsernamePasswordFormAuthenticationListener
    ↪ AbstractAuthenticationListener
    $token = $this->authenticationManager->authenticate(
    new UsernamePasswordToken($username, $password)
    );
    DaoAuthenticationProvider
    ↪ UserAuthenticationProvider
    @weaverryan

    View Slide

  9. Max username length
    User Checker
    Almost no Centralization
    Password checking
    CSRF validation
    Session fixation
    Password upgrading
    AuthSuccessEvent
    ** This is centralized!
    Set token on storage
    In each auth listener In each auth provider
    Auth Manager **
    InteractiveLoginEvent
    Success handler
    Notify RememberMe

    View Slide

  10. What about Guard?
    @weaverryan

    View Slide

  11. Guard: Big Beautiful Bandaid
    GuardAuthenticationListener
    ↪ ->getCredentials()
    ↪ create an un-auth'ed token
    ↪ call AuthenticationManager
    GuardAuthenticationProvider
    ↪ ->getUser()
    ↪ ->checkCredentials()
    ↪ onAuthenticationSuccess()/Failure()
    @weaverryan

    View Slide

  12. Excess Complexity
    Stifles Innovation
    @weaverryan

    View Slide

  13. The New Security Component
    Part 2
    @weaverryan
    experimental in 5.2

    View Slide

  14. 2010
    Today


    View Slide

  15. class User implements UserInterface
    {
    // ...
    public function getUsername(): string
    {
    return (string) $this->email;
    }
    public function getRoles(): array
    {
    $roles = $this->roles;
    $roles[] = 'ROLE_USER';
    return array_unique($roles);
    }
    public function getPassword(): string
    {
    return (string) $this->password;
    }
    public function eraseCredentials()
    {
    }
    }
    User class (Before)
    @weaverryan

    View Slide

  16. class User implements UserInterface
    {
    // ...
    public function getUsername(): string
    {
    return (string) $this->email;
    }
    public function getRoles(): array
    {
    $roles = $this->roles;
    $roles[] = 'ROLE_USER';
    return array_unique($roles);
    }
    public function getPassword(): string
    {
    return (string) $this->password;
    }
    public function eraseCredentials()
    {
    }
    }
    User class (After)
    @weaverryan

    View Slide

  17. class AdminController extends AbstractController
    {
    public function admin()
    {
    $this->denyAccessUnlessGranted('ROLE_ADMIN');
    }
    }
    Denying Access (Before)
    # security.yaml
    security:
    # ...
    access_control:
    - { path: ^/admin, roles: ROLE_ADMIN }
    @weaverryan

    View Slide

  18. class AdminController extends AbstractController
    {
    public function admin()
    {
    $this->denyAccessUnlessGranted('ROLE_ADMIN');
    }
    }
    # security.yaml
    security:
    # ...
    access_control:
    - { path: ^/admin, roles: ROLE_ADMIN }
    Denying Access (After)
    @weaverryan

    View Slide

  19. security.yaml (Before)
    security:
    # ...
    firewalls:
    main:
    lazy: true
    anonymous: true
    form_login: true
    guard:
    authenticators:
    - App\Security\CustomAuthenticator
    @weaverryan

    View Slide

  20. security.yaml (After)
    security:
    # ...
    + enable_authenticator_manager: true
    firewalls:
    main:
    lazy: true
    - anonymous: true
    form_login: true
    - guard:
    - authenticators:
    - - App\Security\CustomAuthenticator
    + custom_authenticators:
    + - App\Security\CustomAuthenticator
    Anonymous users
    are gone!!!
    Authenticator classes
    have a new look
    @weaverryan
    Activates the
    new "mode"

    View Slide

  21. Dont Panic!
    The changes are beautiful.
    The migration path is short.
    @weaverryan

    View Slide

  22. POST /login
    [email protected]
    &password=duck
    Passport
    (badges)
    UserBadge PasswordCredentials CsrfBadge
    events
    CheckCredentialsListener
    CsrfProtectionListener
    SessionStrategyListener
    UserCheckerListener
    LoginThrottlingListener PasswordMigrationListener
    Authenticators
    Firewall

    View Slide

  23. Custom Authenticators
    Part 3
    @weaverryan

    View Slide

  24. New AuthenticatorInterface
    @weaverryan
    interface AuthenticatorInterface
    {
    function supports(Request $request): ?bool;
    function createAuthenticatedToken(PassportInterface $passport): TokenInterface;
    function onAuthenticationSuccess(Request $request, TokenInterface $token): ?Response;
    function onAuthenticationFailure(Request $request, AuthException $exception): ?Response;
    }
    function authenticate(Request $request): PassportInterface;

    View Slide

  25. Example 1:
    Login Form
    @weaverryan

    View Slide

  26. class SecurityController extends AbstractController
    {
    /**
    * @Route("/login", name="app_login")
    */
    public function login(): Response
    {
    return $this->render('security/login.html.twig');
    }
    }



    value="{{ csrf_token('authenticate') }}"
    >

    Sign in


    View Slide

  27. // src/Security/LoginFormAuthenticator
    class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
    {
    public function supports(Request $request): ?bool
    {
    }
    public function authenticate(Request $request): PassportInterface
    {
    }
    public function onAuthenticationSuccess(Request $request, TokenInterface $token): ?Response
    {
    }
    protected function getLoginUrl(Request $request): string
    {
    }
    }

    View Slide

  28. public function supports(Request $request): ?bool
    {
    return $request->attributes->get('_route') === 'app_login'
    && $request->isMethod('POST');
    }

    View Slide

  29. public function authenticate(Request $request): PassportInterface
    {
    $email = $request->request->get('email');
    $password = $request->request->get('password');
    return new Passport(
    new UserBadge($email, function ($email) {
    return $this->userRepository->findOneBy(['email' => $email]);
    }),
    new PasswordCredentials($password)
    );
    }

    View Slide

  30. public function authenticate(Request $request): PassportInterface
    {
    $email = $request->request->get('email');
    $password = $request->request->get('password');
    return new Passport(
    new UserBadge($email, function ($email) {
    return $this->userRepository->findOneBy(['email' => $email]);
    }),
    new PasswordCredentials($password)
    );
    }

    View Slide

  31. public function authenticate(Request $request): PassportInterface
    {
    $email = $request->request->get('email');
    $password = $request->request->get('password');
    return new Passport(
    new UserBadge($email, function ($email) {
    return $this->userRepository->findOneBy(['email' => $email]);
    }),
    new PasswordCredentials($password)
    );
    }

    View Slide

  32. public function authenticate(Request $request): PassportInterface
    {
    $email = $request->request->get('email');
    $password = $request->request->get('password');
    return new Passport(
    new UserBadge($email, function ($email) {
    return $this->userRepository->findOneBy(['email' => $email]);
    }),
    new PasswordCredentials($password)
    );
    }

    View Slide

  33. public function authenticate(Request $request): PassportInterface
    {
    $email = $request->request->get('email');
    $password = $request->request->get('password');
    return new Passport(
    new UserBadge($email, function ($email) {
    return $this->userRepository->findOneBy(['email' => $email]);
    }),
    new PasswordCredentials($password)
    );
    }

    View Slide

  34. Done!
    @weaverryan

    View Slide

  35. User Checker
    Event Listeners: Authentication Work Horses
    Password checking
    CSRF validation
    Session fixation
    Password upgrading
    Set token on storage
    Notify RememberMe
    Events: 6 centralized events

    View Slide

  36. Badges: Extra Passport Info
    @weaverryan
    return new Passport(
    new UserBadge($email, function ($email) {
    // ...
    }),
    new PasswordCredentials($password),
    [
    // badges
    ]
    );

    View Slide

  37. Badges: CSRF Protection
    value="{{ csrf_token('authenticate') }}"
    >
    $csrfToken = $request->request->get('_csrf_token');
    return new Passport(
    new UserBadge($email, function ($email) {
    // ...
    }),
    new PasswordCredentials($password),
    [
    new CsrfTokenBadge('authenticate', $csrfToken)
    ]
    );
    Badges can require
    that a listener
    "resolves" them.

    View Slide

  38. Badges: Password Upgrading
    return new Passport(
    new UserBadge($email, function ($email) {
    // ...
    }),
    new PasswordCredentials($password),
    [
    new CsrfTokenBadge('authenticate', $csrfToken),
    new PasswordUpgradeBadge($password, $this->userRepository)
    ]
    );
    This PasswordUpgradeBadge is no longer required.
    Your password will be upgraded automatically as long
    as your user provider implements the upgrader interface
    (which happens always if you use make:user)

    View Slide

  39. Possibilities?
    Login Throttling & Events
    @weaverryan

    View Slide

  40. security:
    # ...
    firewalls:
    main:
    # ...
    # max at 3 login attempts per minute
    login_throttling:
    max_attempts: 3
    $ composer require symfony/rate-limiter

    View Slide

  41. Events, Events, Events!
    CheckPassportEvent
    Has access to the Passport & all badges
    AuthenticatedTokenCreatedEvent
    Has access - and can change - the authenticated token
    LoginSuccessEvent
    Success! Has Passport, Token, User & Response
    LoginFailureEvent
    Failure :( . But, has to the auth exception, Passport & Response

    View Slide

  42. Legacy Events
    AuthenticationSuccessEvent
    It's fine, but LoginSuccessEvent is more powerful
    InteractiveLoginEvent
    Don't use: what *is* an "interactive" login?
    Don't use these: there are better events now!

    View Slide

  43. And…
    Core Authenticators are Readable!
    @weaverryan

    View Slide

  44. class JsonLoginAuthenticator implements AuthenticatorInterface
    {
    public function authenticate(Request $request): PassportInterface
    {
    $credentials = $this->getCredentials($request);
    $passport = new Passport(new UserBadge($credentials['username'], function ($username) {
    return $this->userProvider->loadUserByUsername($username);
    }), new PasswordCredentials($credentials['password']));
    if ($this->userProvider instanceof PasswordUpgraderInterface) {
    $passport->addBadge(
    new PasswordUpgradeBadge($credentials['password'],
    $this->userProvider
    )
    );
    }
    return $passport;
    }
    }
    json_login

    View Slide

  45. API Token Authentication
    @weaverryan

    View Slide

  46. public function supports(Request $request): ?bool
    {
    return $request->headers->has('X-AUTH-TOKEN');
    }

    View Slide

  47. public function authenticate(Request $request): PassportInterface
    {
    $token = $request->headers->get('X-AUTH-TOKEN');
    return new SelfValidatingPassport(
    new UserBadge($token, function($token) {
    $apiToken = $this->apiTokenRepository->findOneBy([
    'token' => $token
    ]);
    if (null === $apiToken) {
    throw new CustomUserMessageAuthenticationException(
    'Invalid API token'
    );
    }
    return $apiToken->getUser();
    })
    );
    }

    View Slide

  48. public function authenticate(Request $request): PassportInterface
    {
    $token = $request->headers->get('X-AUTH-TOKEN');
    return new SelfValidatingPassport(
    new UserBadge($token, function($token) {
    $apiToken = $this->apiTokenRepository->findOneBy([
    'token' => $token
    ]);
    if (null === $apiToken) {
    throw new CustomUserMessageAuthenticationException(
    'Invalid API token'
    );
    }
    return $apiToken->getUser();
    })
    );
    }

    View Slide

  49. public function authenticate(Request $request): PassportInterface
    {
    $token = $request->headers->get('X-AUTH-TOKEN');
    return new SelfValidatingPassport(
    new UserBadge($token, function($token) {
    $apiToken = $this->apiTokenRepository->findOneBy([
    'token' => $token
    ]);
    if (null === $apiToken) {
    throw new CustomUserMessageAuthenticationException(
    'Invalid API token'
    );
    }
    return $apiToken->getUser();
    })
    );
    }

    View Slide

  50. Various other Improvements
    Part 4
    @weaverryan

    View Slide

  51. Anon. Anonymous Users are Gone!
    security:
    # ...
    access_control:
    - { path: ^/login, roles: PUBLIC_ACCESS }
    - { path: ^/, roles: ROLE_USER }
    • If a user isn't authenticated, the User is null
    • If you need to explicitly allow access:

    View Slide

  52. 2fa No Longer Requires Hacks

    View Slide

  53. LogoutEvent
    class LogoutSubscriber implements EventSubscriberInterface
    {
    public function onLogout(LogoutEvent $event)
    {
    $user = $event->getToken()->getUser();
    // ...
    $response = new RedirectResponse('...');
    $event->setResponse($response);
    }
    public static function getSubscribedEvents()
    {
    return [LogoutEvent::class => 'onLogout'],
    }
    }

    View Slide

  54. Magic Login Link
    security:
    firewalls:
    main:
    login_link:
    check_route: login_check
    signature_properties: ['id']

    View Slide

  55. Magic Login Link
    public function requestLoginLink(LoginLinkHandlerInterface $loginLinkHandler)
    {
    if ($request->isMethod('POST')) {
    $email = $request->request->get('email');
    $user = $userRepository->findOneBy(['email' => $email]);
    $loginLinkDetails = $loginLinkHandler->createLoginLink($user);
    $loginLink = $loginLinkDetails->getUrl();
    // send it with Notifier
    }
    }

    View Slide

  56. Complexity is Gone
    @weaverryan

    View Slide

  57. What else can we make?
    @weaverryan

    View Slide

  58. Me:
    @weaverryan
    THANK YOU!
    Security Hero:
    @wouterj

    View Slide