Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

> 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

Slide 3

Slide 3 text

Security System: A History Part 1 @weaverryan

Slide 4

Slide 4 text

2010 Today … … …

Slide 5

Slide 5 text

@weaverryan What Happened?

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

What about Guard? @weaverryan

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Excess Complexity Stifles Innovation @weaverryan

Slide 13

Slide 13 text

The New Security Component Part 2 @weaverryan experimental in 5.2

Slide 14

Slide 14 text

2010 Today … …

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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"

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Custom Authenticators Part 3 @weaverryan

Slide 24

Slide 24 text

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;

Slide 25

Slide 25 text

Example 1: Login Form @weaverryan

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

// 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 { } }

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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) ); }

Slide 30

Slide 30 text

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) ); }

Slide 31

Slide 31 text

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) ); }

Slide 32

Slide 32 text

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) ); }

Slide 33

Slide 33 text

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) ); }

Slide 34

Slide 34 text

Done! @weaverryan

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Badges: CSRF Protection $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.

Slide 38

Slide 38 text

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)

Slide 39

Slide 39 text

Possibilities? Login Throttling & Events @weaverryan

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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!

Slide 43

Slide 43 text

And… Core Authenticators are Readable! @weaverryan

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

API Token Authentication @weaverryan

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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(); }) ); }

Slide 48

Slide 48 text

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(); }) ); }

Slide 49

Slide 49 text

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(); }) ); }

Slide 50

Slide 50 text

Various other Improvements Part 4 @weaverryan

Slide 51

Slide 51 text

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:

Slide 52

Slide 52 text

2fa No Longer Requires Hacks

Slide 53

Slide 53 text

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'], } }

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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 } }

Slide 56

Slide 56 text

Complexity is Gone @weaverryan

Slide 57

Slide 57 text

What else can we make? @weaverryan

Slide 58

Slide 58 text

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