Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Guard Authentication: Powerful, Beautiful Security

7,328 views

Published on

There are so many interesting ways to authenticate a user: via an API token, social login, a traditional HTML form or anything else you can dream up. But until now, creating a custom authentication system in Symfony has meant a lot of files and a lot of complexity. Introducing Guard: a simple, but expandable authentication system built on top of the security component and introduced in Symfony 2.8. Want to authenticate via an API token? Great - that's just one class. Social login? Easy! Have some crazy legacy central authentication system? In this talk, we'll show you how you'd implement any of these in your application today. Don't get me wrong - you'll still need to do some work. But finally, the path will be clear and joyful.

Published in: Software

Guard Authentication: Powerful, Beautiful Security

  1. 1. Guard Authentication: Powerful, Beautiful Security by your friend: Ryan Weaver @weaverryan
  2. 2. KnpUniversity.com github.com/weaverryan Who is this guy? > Lead for the Symfony documentation
 > KnpLabs US - Symfony Consulting, training & general Kumbaya > Writer for KnpUniversity.com Tutorials > Husband of the much more talented @leannapelham
  3. 3. Introducing… @weaverryan
  4. 4. sfGuardPlugin! @weaverryan
  5. 5. What’s the hardest part of Symfony? @weaverryan
  6. 6. Authentication Who are you? @weaverryan
  7. 7. Authorization Do you have access to do X? @weaverryan
  8. 8. VOTERS! @weaverryan
  9. 9. Authentication in Symfony sucks @weaverryan
  10. 10. 1) Grab information from the request @weaverryan
  11. 11. 2) Load a User @weaverryan
  12. 12. 3) Validate if the credentials are valid @weaverryan
  13. 13. 4) authentication success… now what? @weaverryan
  14. 14. 5) authentication failure … dang, now what?! @weaverryan
  15. 15. 6) How do we “ask” the user to login? @weaverryan
  16. 16. 6 Steps 5 Different Classes @weaverryan
  17. 17. security:
 firewalls:
 main:
 anonymous: ~
 logout: ~
 
 form_login: ~
 http_basic: ~
 some_invented_system_i_created: ~
 Each activates a system of these 5 classes @weaverryan
  18. 18. On Guard! @weaverryan
  19. 19. interface GuardAuthenticatorInterface
 {
 public function getCredentials(Request $request);
 
 public function getUser($credentials, $userProvider);
 
 public function checkCredentials($credentials, UserInterface $user);
 
 public function onAuthenticationFailure(Request $request);
 
 public function onAuthenticationSuccess(Request $request, $token);
 public function start(Request $request); 
 public function supportsRememberMe();
 }
 @weaverryan
  20. 20. Bad News… @weaverryan
  21. 21. It’s getting coal for Christmas!
  22. 22. You still have to do work (sorry Laravel people) @weaverryan
  23. 23. But it will be simple @weaverryan https://github.com/knpuniversity/guard-presentation
  24. 24. You need a User class (This has nothing to do with Guard) @weaverryan
  25. 25. @weaverryan use SymfonyComponentSecurityCoreUserUserInterface;
 
 class User implements UserInterface
 {
 
 }
  26. 26. class User implements UserInterface
 {
 private $username;
 
 public function __construct($username)
 {
 $this->username = $username;
 }
 
 public function getUsername()
 {
 return $this->username;
 }
 
 public function getRoles()
 {
 return ['ROLE_USER'];
 }
 
 // …
 } a unique identifier (not really used anywhere)
  27. 27. @weaverryan class User implements UserInterface
 {
 // … 
 public function getPassword()
 {
 }
 public function getSalt()
 {
 }
 public function eraseCredentials()
 {
 }
 } These are only used for users that have an encoded password
  28. 28. The Hardest Example Ever: Form Login @weaverryan
  29. 29. A traditional login form setup @weaverryan
  30. 30. class SecurityController extends Controller
 {
 /**
 * @Route("/login", name="security_login")
 */
 public function loginAction()
 {
 return $this->render('security/login.html.twig');
 }
 
 /**
 * @Route("/login_check", name="login_check")
 */
 public function loginCheckAction()
 {
 // will never be executed
 }
 }

  31. 31. <form action="{{ path('login_check') }}” method="post">
 <div>
 <label for="username">Username</label>
 <input name="_username" />
 </div>
 
 <div>
 <label for="password">Password:</label>
 <input type="password" name="_password" />
 </div>
 
 <button type="submit">Login</button>
 </form>
  32. 32. Let’s create an authenticator! @weaverryan
  33. 33. class FormLoginAuthenticator extends AbstractGuardAuthenticator
 {
 public function getCredentials(Request $request)
 {
 }
 
 public function getUser($credentials, UserProviderInterface $userProvider)
 {
 }
 
 public function checkCredentials($credentials, UserInterface $user)
 {
 }
 
 public function onAuthenticationFailure(Request $request)
 {
 }
 
 public function onAuthenticationSuccess(Request $request, TokenInterface $token)
 {
 }
 
 public function start(Request $request, AuthenticationException $e = null)
 {
 }
 
 public function supportsRememberMe()
 {
 }
 }
  34. 34. public function getCredentials(Request $request)
 {
 if ($request->getPathInfo() != '/login_check') {
 return;
 }
 
 return [
 'username' => $request->request->get('_username'),
 'password' => $request->request->get('_password'),
 ];
 } Grab the “login” credentials! @weaverryan
  35. 35. public function getUser($credentials, UserProviderInterface $userProvider)
 {
 $username = $credentials['username'];
 
 $user = new User();
 $user->setUsername($username);
 
 return $user;
 } Create/Load that User! @weaverryan
  36. 36. public function checkCredentials($credentials, UserInterface $user)
 {
 $password = $credentials['password'];
 if ($password == 'santa' || $password == 'elves') {
 return;
 }
 
 return true;
 } Are the credentials correct? @weaverryan
  37. 37. public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
 {
 $url = $this->router->generate('security_login');
 
 return new RedirectResponse($url);
 } Crap! Auth failed! Now what!? @weaverryan
  38. 38. public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
 {
 $url = $this->router->generate('homepage');
 
 return new RedirectResponse($url);
 } Amazing. Auth worked. Now what? @weaverryan
  39. 39. public function start(Request $request)
 {
 $url = $this->router->generate('security_login');
 
 return new RedirectResponse($url);
 } Anonymous user went to /admin now what? @weaverryan
  40. 40. Register as a service services:
 form_login_authenticator:
 class: AppBundleSecurityFormLoginAuthenticator
 arguments: [‘@router’]
 @weaverryan
  41. 41. Activate in your firewall security:
 firewalls:
 main:
 anonymous: ~
 logout: ~
 guard:
 authenticators:
 - form_login_authenticator @weaverryan
  42. 42. AbstractFormLoginAuthenticator Free login form authenticator code @weaverryan
  43. 43. User Providers (This has nothing to do with Guard) @weaverryan
  44. 44. Every App has a User class @weaverryan And the Christmas spirit
  45. 45. Each User Class Needs 1 User Provider @weaverryan
  46. 46. class FestiveUserProvider implements UserProviderInterface
 {
 public function loadUserByUsername($username)
 {
 // "load" the user - e.g. load from the db
 $user = new User();
 $user->setUsername($username);
 
 return $user;
 }
 
 public function refreshUser(UserInterface $user)
 {
 return $user;
 }
 
 public function supportsClass($class)
 {
 return $class == 'AppBundleEntityUser';
 }
 }
  47. 47. services:
 festive_user_provider:
 class: AppBundleSecurityFestiveUserProvider
 @weaverryan
  48. 48. security:
 providers:
 elves:
 id: festive_user_provider
 
 firewalls:
 main:
 anonymous: ~
 logout: ~
 # this is optional as there is only 1 provider
 provider: elves
 guard:
 authenticators: [form_login_authenticator]
 Boom! Optional Boom!
  49. 49. class FestiveUserProvider implements UserProviderInterface
 {
 public function loadUserByUsername($username)
 {
 // "load" the user - e.g. load from the db
 $user = new User();
 $user->setUsername($username);
 
 return $user;
 }
 
 public function refreshUser(UserInterface $user)
 {
 return $user;
 }
 
 public function supportsClass($class)
 {
 return $class == 'AppBundleEntityUser';
 }
 } But why!?
  50. 50. class FestiveUserProvider implements UserProviderInterface
 {
 public function loadUserByUsername($username)
 {
 // "load" the user - e.g. load from the db
 $user = new User();
 $user->setUsername($username);
 
 return $user;
 }
 
 public function refreshUser(UserInterface $user)
 {
 return $user;
 }
 
 public function supportsClass($class)
 {
 return $class == 'AppBundleEntityUser';
 }
 } refresh from the session
  51. 51. class FestiveUserProvider implements UserProviderInterface
 {
 public function loadUserByUsername($username)
 {
 // "load" the user - e.g. load from the db
 $user = new User();
 $user->setUsername($username);
 
 return $user;
 }
 
 public function refreshUser(UserInterface $user)
 {
 return $user;
 }
 
 public function supportsClass($class)
 {
 return $class == 'AppBundleEntityUser';
 }
 } switch_user, remember_me
  52. 52. Slightly more wonderful: Loading a User from the Database @weaverryan
  53. 53. class FestiveUserProvider implements UserProviderInterface
 {
 public function loadUserByUsername($username)
 {
 $user = $this->em->getRepository('AppBundle:User')
 ->findOneBy(['username' => $username]);
 
 if (!$user) {
 throw new UsernameNotFoundException();
 }
 
 return $user;
 }
 } @weaverryan (of course, the “entity” user provider does this automatically)
  54. 54. public function getUser($credentials, UserProviderInterface $userProvider)
 {
 $username = $credentials['username'];
 //return $userProvider->loadUserByUsername($username);
 
 return $this->em
 ->getRepository('AppBundle:User')
 ->findOneBy(['username' => $username]);
 } FormLoginAuthenticator you can use this if you want to … or don’t!
  55. 55. Easiest Example ever: Api Token Authentication @weaverryan Jolliest
  56. 56. 1) Client sends a token on an X- API-TOKEN header 2) We load a User associated with that token class User implements UserInterface
 {
 /**
 * @ORMColumn(type="string")
 */
 private $apiToken;
 
 // ...
 }
  57. 57. class ApiTokenAuthenticator extends AbstractGuardAuthenticator
 {
 public function getCredentials(Request $request)
 {
 }
 
 public function getUser($credentials, UserProviderInterface $userProvider)
 {
 }
 
 public function checkCredentials($credentials, UserInterface $user)
 {
 }
 
 public function onAuthenticationFailure(Request $request)
 {
 }
 
 public function onAuthenticationSuccess(Request $request, TokenInterface $token)
 {
 }
 
 public function start(Request $request, AuthenticationException $e = null)
 {
 }
 
 public function supportsRememberMe()
 {
 }
 }
  58. 58. public function getCredentials(Request $request)
 {
 return $request->headers->get('X-API-TOKEN');
 } @weaverryan
  59. 59. public function getUser($credentials, UserProviderInterface $userProvider)
 {
 $apiToken = $credentials;
 
 return $this->em
 ->getRepository('AppBundle:User')
 ->findOneBy(['apiToken' => $apiToken]);
 } @weaverryan
  60. 60. OR If you use JWT, get the payload from the token and load the User from it @weaverryan
  61. 61. public function checkCredentials($credentials, UserInterface $user)
 {
 // no credentials to check
 return true;
 }
 @weaverryan
  62. 62. public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
 {
 return new JsonResponse([
 'message' => $exception->getMessageKey()
 ], 401);
 } @weaverryan
  63. 63. public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
 {
 // let the request continue to the controller
 return;
 } @weaverryan
  64. 64. Register as a service services:
 api_token_authenticator:
 class: AppBundleSecurityApiTokenAuthenticator
 arguments:
 - '@doctrine.orm.entity_manager' @weaverryan
  65. 65. Activate in your firewall security:
 # ...
 firewalls:
 main:
 # ...
 guard:
 authenticators:
 - form_login_authenticator
 - api_token_authenticator
 entry_point: form_login_authenticator
 which “start” method should be called
  66. 66. curl http://localhost:8000/secure <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta http-equiv="refresh" content="1;url=/login" /> <title>Redirecting to /login</title> </head> <body> Redirecting to <a href="/login">/login</a>. </body> </html> @weaverryan
  67. 67. curl --header "X-API-TOKEN: BAD" http://localhost:8000/secure {"message":"Username could not be found."} @weaverryan
  68. 68. curl --header "X-API-TOKEN: GOOD" http://localhost:8000/secure {"message":"Hello from the secureAction!"} @weaverryan
  69. 69. Social Login! @weaverryan
  70. 70. ! AUTHENTICATOR /facebook/check?code=abc give me user info! load a User object " User !
  71. 71. composer require league/oauth2-facebook @weaverryan
  72. 72. @weaverryan services:
 app.facebook_provider:
 class: LeagueOAuth2ClientProviderFacebook
 arguments:
 -
 clientId: %facebook_app_id%
 clientSecret: %facebook_app_secret%
 graphApiVersion: v2.3
 redirectUri: "..."
 @=service('router').generate('connect_facebook_check', {}, true)
  73. 73. @weaverryan public function connectFacebookAction()
 {
 // redirect to Facebook
 $facebookOAuthProvider = $this->get('app.facebook_provider');
 
 $url = $facebookOAuthProvider->getAuthorizationUrl([
 // these are actually the default scopes
 'scopes' => ['public_profile', 'email'],
 ]);
 
 return $this->redirect($url);
 }
 
 /**
 * @Route("/connect/facebook-check", name="connect_facebook_check")
 */
 public function connectFacebookActionCheck()
 {
 // will not be reached!
 }
  74. 74. class FacebookAuthenticator extends AbstractGuardAuthenticator
 {
 public function getCredentials(Request $request)
 {
 }
 
 public function getUser($credentials, UserProviderInterface $userProvider)
 {
 }
 
 public function checkCredentials($credentials, UserInterface $user)
 {
 }
 
 public function onAuthenticationFailure(Request $request)
 {
 }
 
 public function onAuthenticationSuccess(Request $request, TokenInterface $token)
 {
 }
 
 public function start(Request $request, AuthenticationException $e = null)
 {
 }
 
 public function supportsRememberMe()
 {
 }
 }
  75. 75. public function getCredentials(Request $request)
 {
 if ($request->getPathInfo() != '/connect/facebook-check') {
 return;
 }
 
 return $request->query->get('code');
 } @weaverryan
  76. 76. public function getUser($credentials, …)
 {
 $authorizationCode = $credentials;
 
 $facebookProvider = $this->container->get('app.facebook_provider');
 
 $accessToken = $facebookProvider->getAccessToken(
 'authorization_code',
 ['code' => $authorizationCode]
 );
 
 /** @var FacebookUser $facebookUser */
 $facebookUser = $facebookProvider->getResourceOwner($accessToken);
 
 // ...
 } @weaverryan
  77. 77. Now, have some hot chocolate! @weaverryan
  78. 78. public function getUser($credentials, …)
 { // ... 
 /** @var FacebookUser $facebookUser */
 $facebookUser = $facebookProvider->getResourceOwner($accessToken);
 
 // ...
 $em = $this->container->get('doctrine')->getManager();
 
 // 1) have they logged in with Facebook before? Easy!
 $user = $em->getRepository('AppBundle:User')
 ->findOneBy(array('email' => $facebookUser->getEmail())); 
 if ($user) {
 return $user;
 } 
 // ...
 } @weaverryan
  79. 79. public function getUser($credentials, ...)
 {
 // ...
 
 // 2) no user? Perhaps you just want to create one
 // (or redirect to a registration)
 $user = new User();
 $user->setUsername($facebookUser->getName());
 $user->setEmail($facebookUser->getEmail());
 $em->persist($user);
 $em->flush(); return $user;
 } @weaverryan
  80. 80. public function checkCredentials($credentials, UserInterface $user)
 {
 // nothing to do here!
 }
 
 public function onAuthenticationFailure(Request $request ...)
 {
 // redirect to login
 }
 
 public function onAuthenticationSuccess(Request $request ...)
 {
 // redirect to homepage / last page
 } @weaverryan
  81. 81. Extra Treats (no coal) @weaverryan
  82. 82. Can I control the error message? @weaverryan
  83. 83. @weaverryan If authentication failed, it is because an AuthenticationException (or sub-class) was thrown (This has nothing to do with Guard)
  84. 84. public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
 {
 return new JsonResponse([
 'message' => $exception->getMessageKey()
 ], 401);
 } @weaverryan Christmas miracle! The exception is passed when authentication fails AuthenticationException has a hardcoded getMessageKey() “safe” string Invalid credentials.
  85. 85. public function getCredentials(Request $request)
 {
 }
 
 public function getUser($credentials, UserProviderInterface $userProvider)
 {
 }
 
 public function checkCredentials($credentials, UserInterface $user)
 {
 }
 Throw an AuthenticationException at any time in these 3 methods
  86. 86. How can I customize the message? @weaverryan Create a new sub-class of AuthenticationException for each message and override getMessageKey()
  87. 87. CustomUserMessageAuthenticationException @weaverryan
  88. 88. public function getUser($credentials, ...)
 {
 $apiToken = $credentials;
 
 $user = $this->em
 ->getRepository('AppBundle:User')
 ->findOneBy(['apiToken' => $apiToken]);
 
 if (!$user) {
 throw new CustomUserMessageAuthenticationException(
 'That API token is not very jolly'
 );
 }
 
 return $user;
 } @weaverryan
  89. 89. I need to manually authenticate my user @weaverryan
  90. 90. public function registerAction(Request $request)
 {
 $user = new User();
 $form = // ...
 
 if ($form->isValid()) {
 // save the user
 
 $guardHandler = $this->container
 ->get('security.authentication.guard_handler');
 
 $guardHandler->authenticateUserAndHandleSuccess(
 $user,
 $request,
 $this->get('form_login_authenticator'),
 'main' // the name of your firewall
 );
 // redirect
 }
 // ...
 }
  91. 91. I want to save a lastLoggedInAt field on my user no matter *how* they login @weaverryan
  92. 92. Chill… that was already possible SecurityEvents::INTERACTIVE_LOGIN @weaverryan
  93. 93. class LastLoginSubscriber implements EventSubscriberInterface
 {
 public function onInteractiveLogin(InteractiveLoginEvent $event)
 {
 /** @var User $user */
 $user = $event->getAuthenticationToken()->getUser();
 $user->setLastLoginTime(new DateTime());
 $this->em->persist($user);
 $this->em->flush($user);
 }
 
 public static function getSubscribedEvents()
 {
 return [
 SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin'
 ];
 }
 }
 @weaverryan
  94. 94. All of these features are available now! @weaverryan Thanks 2.8!
  95. 95. KnpUGuardBundle @weaverryan For those on 2.7
  96. 96. knpuniversity.com/guard
  97. 97. @weaverryan Ok, what just happened?
  98. 98. 1. User implements UserInterface @weaverryan
  99. 99. 2. UserProvider @weaverryan
  100. 100. 3. Create your authenticator(s) @weaverryan
  101. 101. Authentication# @weaverryan
  102. 102. @weaverryan PHP & Symfony Video Tutorials KnpUniversity.com Thank You!

×