Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. > 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
  2. Massive, massive complexity Original implementation based off of Java Spring

    Powerful Complex … almost no-one understood how it worked @weaverryan
  3. 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
  4. The Simple form_login UsernamePasswordFormAuthenticationListener ↪ AbstractAuthenticationListener $token = $this->authenticationManager->authenticate( new

    UsernamePasswordToken($username, $password) ); DaoAuthenticationProvider ↪ UserAuthenticationProvider @weaverryan
  5. 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
  6. Guard: Big Beautiful Bandaid GuardAuthenticationListener ↪ ->getCredentials() ↪ create an

    un-auth'ed token ↪ call AuthenticationManager GuardAuthenticationProvider ↪ ->getUser() ↪ ->checkCredentials() ↪ onAuthenticationSuccess()/Failure() @weaverryan
  7. 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
  8. 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
  9. 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
  10. 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
  11. security.yaml (Before) security: # ... firewalls: main: lazy: true anonymous:

    true form_login: true guard: authenticators: - App\Security\CustomAuthenticator @weaverryan
  12. 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"
  13. POST /login [email protected] &password=duck Passport (badges) UserBadge PasswordCredentials CsrfBadge events

    CheckCredentialsListener CsrfProtectionListener SessionStrategyListener UserCheckerListener LoginThrottlingListener PasswordMigrationListener Authenticators Firewall
  14. 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;
  15. class SecurityController extends AbstractController { /** * @Route("/login", name="app_login") */

    public function login(): Response { return $this->render('security/login.html.twig'); } } <form method="post"> <input type="email" name="email"> <input type="password" name="password"> <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}" > <button type="submit"> Sign in </button> </form>
  16. // 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 { } }
  17. 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) ); }
  18. 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) ); }
  19. 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) ); }
  20. 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) ); }
  21. 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) ); }
  22. 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
  23. Badges: Extra Passport Info @weaverryan return new Passport( new UserBadge($email,

    function ($email) { // ... }), new PasswordCredentials($password), [ // badges ] );
  24. Badges: CSRF Protection <input type="hidden" name="_csrf_token" 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.
  25. 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)
  26. security: # ... firewalls: main: # ... # max at

    3 login attempts per minute login_throttling: max_attempts: 3 $ composer require symfony/rate-limiter
  27. 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
  28. 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!
  29. 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
  30. 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(); }) ); }
  31. 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(); }) ); }
  32. 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(); }) ); }
  33. 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:
  34. 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'], } }
  35. 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 } }