Successfully reported this slideshow.
Your SlideShare is downloading. ×

Symfony Guard Authentication: Fun with API Token, Social Login, JWT and more

Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Loading in …3
×

Check these out next

1 of 114 Ad

Symfony Guard Authentication: Fun with API Token, Social Login, JWT and more

Download to read offline

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 Symfony's security component. 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.

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 Symfony's security component. 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.

Advertisement
Advertisement

More Related Content

Slideshows for you (20)

Similar to Symfony Guard Authentication: Fun with API Token, Social Login, JWT and more (20)

Advertisement

More from Ryan Weaver (18)

Recently uploaded (20)

Advertisement

Symfony Guard Authentication: Fun with API Token, Social Login, JWT and more

  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. KnpUniversity.com
  4. 4. KnpUniversity.com
  5. 5. 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
  6. 6. What’s the hardest part of Symfony? @weaverryan
  7. 7. Authentication* Who are you? @weaverryan *I am hard
  8. 8. Authorization Do you have access to do X? @weaverryan
  9. 9. VOTERS! @weaverryan
  10. 10. Authentication in Symfony sucks? @weaverryan
  11. 11. 1) Grab information from the request @weaverryan
  12. 12. 2) Load a User @weaverryan
  13. 13. 3) Validate if the credentials are valid @weaverryan
  14. 14. 4) authentication success… now what? @weaverryan
  15. 15. 5) authentication failure … dang, now what?! @weaverryan
  16. 16. 6) How do we “ask” the user to login? @weaverryan
  17. 17. 6 Steps 5 Different Classes @weaverryan
  18. 18. security:
 firewalls:
 main:
 anonymous: ~
 logout: ~
 
 form_login: ~
 http_basic: ~
 some_invented_system_i_created: ~
 Each activates a system of these 5 classes @weaverryan
  19. 19. Authentication in Symfony sucks? @weaverryan
  20. 20. On Guard! @weaverryan
  21. 21. 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
  22. 22. Bad News… @weaverryan
  23. 23. You still have to do work! @weaverryan and wear sunscreen!
  24. 24. But it will be simple @weaverryan https://github.com/knpuniversity/guard-presentation
  25. 25. You need a User class (This has nothing to do with Guard) @weaverryan ☼
  26. 26. @weaverryan use SymfonyComponentSecurityCoreUserUserInterface;
 
 class User implements UserInterface
 {
 
 }
  27. 27. 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)
  28. 28. @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
  29. 29. The Hardest Example Ever: Form Login @weaverryan ☼ ☼
  30. 30. A traditional login form setup @weaverryan
  31. 31. 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
 }
 }

  32. 32. <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>
  33. 33. Let’s create an authenticator! @weaverryan
  34. 34. 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()
 {
 }
 }
  35. 35. 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
  36. 36. public function getUser($credentials, UserProviderInterface $userProvider)
 {
 $username = $credentials['username'];
 
 $user = new User();
 $user->setUsername($username);
 
 return $user;
 } Create/Load that User! @weaverryan
  37. 37. public function checkCredentials($credentials, UserInterface $user)
 {
 $password = $credentials['password'];
 if ($password == 'santa' || $password == 'elves') {
 return;
 }
 
 return true;
 } Are the credentials correct? @weaverryan
  38. 38. public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
 {
 $url = $this->router->generate('security_login');
 
 return new RedirectResponse($url);
 } Crap! Auth failed! Now what!? @weaverryan
  39. 39. public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
 {
 $url = $this->router->generate('homepage');
 
 return new RedirectResponse($url);
 } Amazing. Auth worked. Now what? @weaverryan
  40. 40. public function start(Request $request)
 {
 $url = $this->router->generate('security_login');
 
 return new RedirectResponse($url);
 } Anonymous user went to /admin now what? @weaverryan
  41. 41. Register as a service services:
 form_login_authenticator:
 class: AppBundleSecurityFormLoginAuthenticator
 autowire: true
 @weaverryan
  42. 42. Activate in your firewall security:
 firewalls:
 main:
 anonymous: ~
 logout: ~
 guard:
 authenticators:
 - form_login_authenticator @weaverryan
  43. 43. User Providers (This has nothing to do with Guard) @weaverryan ☼☼ ☼
  44. 44. Each App has a User class @weaverryan And the Christmas spirit
  45. 45. Each User Class Needs 1 User Provider @weaverryan
  46. 46. class SunnyUserProvider 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:
 sunny_user_provider:
 class: AppBundleSecuritySunnyUserProvider
 @weaverryan
  48. 48. security:
 providers:
 sunnny_users:
 id: sunny_user_provider
 
 firewalls:
 main:
 anonymous: ~
 logout: ~
 # this is optional as there is only 1 provider
 provider: sunny_users
 guard:
 authenticators: [form_login_authenticator]
 Boom! Optional Boom!
  49. 49. class SunnyUserProvider 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 SunnyUserProvider 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 SunnyUserProvider 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 sunshiney: Loading a User from the Database @weaverryan
  53. 53. 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!
  54. 54. class SunnyUserProvider 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)
  55. 55. Easiest Example ever: Api (JWT) Authentication @weaverryan Warmest
  56. 56. JSON Web Tokens @weaverryan Q) What if an API client could simply send you its user id as authentication? Authorization: Bearer 123
  57. 57. 1) API client authenticates API client Hey dude, I’m weaverryan POST /token username=weaverryan password=I<3php app
  58. 58. 2) Is this really weaverryan? API client “It checks out, weaverryan’s password really is I<3php. Nerd” POST /token username=weaverryan password=I<3php app
  59. 59. 3) Create a package of data API client app $data = [
 'username' => 'weaverryan'
 ];
  60. 60. 4) Sign the data! $data = [
 'username' => 'weaverryan'
 ];
 
 // package: namshi/jose
 $jws = new SimpleJWS(['alg' => 'RS256']);
 $jws->setPayload($data);
 
 $privateKey = openssl_pkey_get_private(
 'file://path/to/private.key'
 );
 $jws->sign($privateKey);
 
 $token = $jws->getTokenString()

  61. 61. 5) Send the token back! API client app {
 "token": "big_long_json_webtoken"
 } POST /token username=weaverryan password=I<3php
  62. 62. 6) Client sends the token API client app GET /secret/stuff Authorization: Bearer big_login_json_webtoken
  63. 63. 7) Verify the signature // "Authorization: Bearer 123" -> "123"
 $authHeader = $request->headers->get('Authorization'); $headerParts = explode(' ', $authHeader);
 $token = $headerParts[1];
 
 $jws = SimpleJWS::load($token);
 $public_key = openssl_pkey_get_public(
 '/path/to/public.key'
 );
 if (!$jws->isValid($public_key, 'RS256')) {
 die('go away >:(')
 }
  64. 64. 8) Decode the token 
 $payload = $jws->getPayload();
 
 $username = $payload['username'];

  65. 65. How in Symfony? @weaverryan
  66. 66. @weaverryan 1) Install a library to help sign tokens composer require lexik/jwt-authentication-bundle
  67. 67. @weaverryan 2) Create a public & private key mkdir var/jwt openssl genrsa -out var/jwt/private.pem 4096 openssl rsa -pubout -in var/jwt/private.pem -out var/jwt/public.pem
  68. 68. @weaverryan 3) Point the library at them # app/config/config.yml
 lexik_jwt_authentication:
 private_key_path: %kernel.root_dir%/../var/jwt/private.pem
 public_key_path: %kernel.root_dir%/../var/jwt/public.pem

  69. 69. 4) Endpoint to return tokens /**
 * @Route("/token")
 */
 public function fetchToken(Request $request)
 {
 $username = $request->request->get('username');
 $password = $request->request->get('password');
 
 $user = $this->getDoctrine()
 ->getRepository('AppBundle:User')
 ->findOneBy(['username' => $username]);
 if (!$user) {
 throw $this->createNotFoundException();
 }
 
 // check password
 
 $token = $this->get('lexik_jwt_authentication.encoder')
 ->encode(['username' => $user->getUsername()]);
 
 return new JsonResponse(['token' => $token]);
 }
  70. 70. 5) Create the JWT Authenticator
  71. 71. class JwtAuthenticator extends AbstractGuardAuthenticator
 { private $em;
 private $jwtEncoder;
 
 public function __construct(EntityManager $em, JWTEncoder $jwtEncoder)
 {
 $this->em = $em;
 $this->jwtEncoder = $jwtEncoder;
 } 
 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)
 {
 }
 
 // …
 }
  72. 72. public function getCredentials(Request $request)
 {
 $extractor = new AuthorizationHeaderTokenExtractor(
 'Bearer',
 'Authorization'
 );
 
 $token = $extractor->extract($request);
 
 if (false === $token) {
 return;
 }
 
 return $token;
 } @weaverryan
  73. 73. public function getUser($credentials, UserProviderInterface $userProvider)
 {
 $data = $this->jwtEncoder->decode($credentials);
 
 if (!$data) {
 return;
 }
 
 $username = $data['username'];
 
 return $this->em
 ->getRepository('AppBundle:User')
 ->findOneBy(['username' => $username]);
 } @weaverryan
  74. 74. public function checkCredentials($credentials, UserInterface $user)
 {
 // no credentials to check
 return true;
 }
 @weaverryan
  75. 75. public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
 {
 return new JsonResponse([
 'message' => $exception->getMessageKey()
 ], 401);
 } @weaverryan
  76. 76. public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
 {
 // let the request continue to the controller
 return;
 } @weaverryan
  77. 77. Register as a service # app/config/services.yml
 services:
 jwt_authenticator:
 class: AppBundleSecurityJwtAuthenticator
 autowire: true @weaverryan
  78. 78. Activate in your firewall security:
 # ...
 firewalls:
 main:
 # ...
 guard:
 authenticators:
 - form_login_authenticator
 - jwt_authenticator
 entry_point: form_login_authenticator
 which “start” method should be called
  79. 79. 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
  80. 80. curl --header “Authorization: Bearer BAD" http://localhost:8000/secure {"message":"Username could not be found."} @weaverryan
  81. 81. curl --header “Authorization: Bearer GOOD" http://localhost:8000/secure {"message":"Hello from the secureAction!"} @weaverryan
  82. 82. Social Login! @weaverryan ☼
  83. 83. AUTHENTICATOR /facebook/check?code=abc give me user info! load a User object User
  84. 84. composer require league/oauth2-facebook knpuniversity/oauth2-client-bundle @weaverryan
  85. 85. @weaverryan # app/config/config.yml
 knpu_oauth2_client:
 clients:
 # creates service: "knpu.oauth2.client.facebook"
 facebook:
 type: facebook
 client_id: %facebook_client_id%
 client_secret: %facebook_client_secret%
 redirect_route: connect_facebook_check
 graph_api_version: v2.5
  86. 86. @weaverryan /**
 * @Route("/connect/facebook", name="connect_facebook")
 */
 public function connectFacebookAction()
 {
 return $this->get('knpu.oauth2.client.facebook')
 ->redirect(['public_profile', 'email']);
 }
 
 /**
 * @Route("/connect/facebook-check", name="connect_facebook_check")
 */
 public function connectFacebookActionCheck()
 {
 // will not be reached!
 }
  87. 87. 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()
 {
 }
 }
  88. 88. public function getCredentials(Request $request)
 {
 if ($request->getPathInfo() != '/connect/facebook-check') {
 return;
 }
 
 return $this->oAuth2Client->getAccessToken($request);
 } @weaverryan
  89. 89. public function getUser($credentials, …)
 {
 /** @var AccessToken $accessToken */
 $accessToken = $credentials;
 
 /** @var FacebookUser $facebookUser */
 $facebookUser = $this->oAuth2Client
 ->fetchUserFromToken($accessToken);
 
 // ...
 } @weaverryan
  90. 90. Now, relax in the shade! @weaverryan
  91. 91. @weaverryan public function getUser($credentials, ...)
 {
 // ...
 
 /** @var FacebookUser $facebookUser */
 $facebookUser = $this->oAuth2Client
 ->fetchUserFromToken($accessToken);
 
 // 1) have they logged in with Facebook before? Easy!
 $user = $this->em->getRepository('AppBundle:User')
 ->findOneBy(array('email' => $facebookUser->getEmail()));
 
 if ($user) {
 return $user;
 }
 
 // ...
 }
  92. 92. 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
  93. 93. 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
  94. 94. * also supports “finishing registration” @weaverryan
  95. 95. Extra Sunshine (no sunburn) @weaverryan
  96. 96. Can I control the error message? @weaverryan
  97. 97. @weaverryan If authentication failed, it is because an AuthenticationException (or sub-class) was thrown (This has nothing to do with Guard)
  98. 98. public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
 {
 return new JsonResponse([
 'message' => $exception->getMessageKey()
 ], 401);
 } @weaverryan Beach Vacation Bonus! The exception is passed when authentication fails AuthenticationException has a hardcoded getMessageKey() “safe” string Invalid credentials.
  99. 99. 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
  100. 100. How can I customize the message? @weaverryan Create a new sub-class of AuthenticationException for each message and override getMessageKey()
  101. 101. CustomUserMessageAuthenticationException @weaverryan
  102. 102. public function getUser($credentials, ...)
 {
 $apiToken = $credentials;
 
 $user = $this->em
 ->getRepository('AppBundle:User')
 ->findOneBy(['apiToken' => $apiToken]);
 
 if (!$user) {
 throw new CustomUserMessageAuthenticationException(
 'That API token is stormy'
 );
 }
 
 return $user;
 } @weaverryan
  103. 103. I need to manually authenticate my user @weaverryan
  104. 104. 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
 }
 // ...
 }
  105. 105. I want to save a lastLoggedInAt field on my user no matter *how* they login @weaverryan
  106. 106. Chill… that was already possible SecurityEvents::INTERACTIVE_LOGIN @weaverryan
  107. 107. 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
  108. 108. @weaverryan Ok, so how do I make my weird auth system?
  109. 109. 1. User implements UserInterface @weaverryan
  110. 110. 2. UserProvider @weaverryan
  111. 111. 3. Create your authenticator(s) @weaverryan
  112. 112. Authentication @weaverryan
  113. 113. @weaverryan Do it: http://symfony.com/doc/current/cookbook/security/guard-authentication.html http://KnpUniversity.com/guard
  114. 114. @weaverryan New (free) Symfony 3 Tutorial KnpUniversity.com Thank You!

×