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

<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>
Let’s create an authenticator! @weaverryan
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()
 {
 }
 }
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
public function getUser($credentials, UserProviderInterface $userProvider)
 {
 $username = $credentials['username'];
 
 $user = new User();
 $user->setUsername($username);
 
 return $user;
 } Create/Load that User! @weaverryan
public function checkCredentials($credentials, UserInterface $user)
 {
 $password = $credentials['password'];
 if ($password == 'santa' || $password == 'elves') {
 return;
 }
 
 return true;
 } Are the credentials correct? @weaverryan
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
 {
 $url = $this->router->generate('security_login');
 
 return new RedirectResponse($url);
 } Crap! Auth failed! Now what!? @weaverryan
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
 {
 $url = $this->router->generate('homepage');
 
 return new RedirectResponse($url);
 } Amazing. Auth worked. Now what? @weaverryan
public function start(Request $request)
 {
 $url = $this->router->generate('security_login');
 
 return new RedirectResponse($url);
 } Anonymous user went to /admin now what? @weaverryan
Register as a service services:
 form_login_authenticator:
 class: AppBundleSecurityFormLoginAuthenticator
 autowire: true
 @weaverryan
Activate in your firewall security:
 firewalls:
 main:
 anonymous: ~
 logout: ~
 guard:
 authenticators:
 - form_login_authenticator @weaverryan
User Providers (This has nothing to do with Guard) @weaverryan ☼☼ ☼
Each App has a User class @weaverryan And the Christmas spirit
Each User Class Needs 1 User Provider @weaverryan
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';
 }
 }
services:
 sunny_user_provider:
 class: AppBundleSecuritySunnyUserProvider
 @weaverryan
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!
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!?
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
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
Slightly more sunshiney: Loading a User from the Database @weaverryan
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!
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)
Easiest Example ever: Api (JWT) Authentication @weaverryan Warmest
JSON Web Tokens @weaverryan Q) What if an API client could simply send you its user id as authentication? Authorization: Bearer 123
1) API client authenticates API client Hey dude, I’m weaverryan POST /token username=weaverryan password=I<3php app
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
3) Create a package of data API client app $data = [
 'username' => 'weaverryan'
 ];
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()

5) Send the token back! API client app {
 "token": "big_long_json_webtoken"
 } POST /token username=weaverryan password=I<3php
6) Client sends the token API client app GET /secret/stuff Authorization: Bearer big_login_json_webtoken
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 >:(')
 }
8) Decode the token 
 $payload = $jws->getPayload();
 
 $username = $payload['username'];

How in Symfony? @weaverryan
@weaverryan 1) Install a library to help sign tokens composer require lexik/jwt-authentication-bundle
@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
@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

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]);
 }
5) Create the JWT Authenticator
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)
 {
 }
 
 // …
 }
public function getCredentials(Request $request)
 {
 $extractor = new AuthorizationHeaderTokenExtractor(
 'Bearer',
 'Authorization'
 );
 
 $token = $extractor->extract($request);
 
 if (false === $token) {
 return;
 }
 
 return $token;
 } @weaverryan
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
public function checkCredentials($credentials, UserInterface $user)
 {
 // no credentials to check
 return true;
 }
 @weaverryan
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
 {
 return new JsonResponse([
 'message' => $exception->getMessageKey()
 ], 401);
 } @weaverryan
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
 {
 // let the request continue to the controller
 return;
 } @weaverryan
Register as a service # app/config/services.yml
 services:
 jwt_authenticator:
 class: AppBundleSecurityJwtAuthenticator
 autowire: true @weaverryan
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
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
curl --header “Authorization: Bearer BAD" http://localhost:8000/secure {"message":"Username could not be found."} @weaverryan
curl --header “Authorization: Bearer GOOD" http://localhost:8000/secure {"message":"Hello from the secureAction!"} @weaverryan
Social Login! @weaverryan ☼
AUTHENTICATOR /facebook/check?code=abc give me user info! load a User object User
composer require league/oauth2-facebook knpuniversity/oauth2-client-bundle @weaverryan
@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
@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!
 }
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()
 {
 }
 }
public function getCredentials(Request $request)
 {
 if ($request->getPathInfo() != '/connect/facebook-check') {
 return;
 }
 
 return $this->oAuth2Client->getAccessToken($request);
 } @weaverryan
public function getUser($credentials, …)
 {
 /** @var AccessToken $accessToken */
 $accessToken = $credentials;
 
 /** @var FacebookUser $facebookUser */
 $facebookUser = $this->oAuth2Client
 ->fetchUserFromToken($accessToken);
 
 // ...
 } @weaverryan
Now, relax in the shade! @weaverryan
@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;
 }
 
 // ...
 }
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
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
* also supports “finishing registration” @weaverryan
Extra Sunshine (no sunburn) @weaverryan
Can I control the error message? @weaverryan
@weaverryan If authentication failed, it is because an AuthenticationException (or sub-class) was thrown (This has nothing to do with Guard)
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.
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
How can I customize the message? @weaverryan Create a new sub-class of AuthenticationException for each message and override getMessageKey()
CustomUserMessageAuthenticationException @weaverryan
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
I need to manually authenticate my user @weaverryan
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
 }
 // ...
 }
I want to save a lastLoggedInAt field on my user no matter *how* they login @weaverryan
Chill… that was already possible SecurityEvents::INTERACTIVE_LOGIN @weaverryan
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
@weaverryan Ok, so how do I make my weird auth system?
1. User implements UserInterface @weaverryan
2. UserProvider @weaverryan
3. Create your authenticator(s) @weaverryan
Authentication @weaverryan
@weaverryan Do it: http://symfony.com/doc/current/cookbook/security/guard-authentication.html http://KnpUniversity.com/guard
@weaverryan New (free) Symfony 3 Tutorial KnpUniversity.com Thank You!

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

  • 1.
    Guard Authentication: Powerful, BeautifulSecurity by your friend: Ryan Weaver @weaverryan
  • 2.
    KnpUniversity.com github.com/weaverryan Who is thisguy? > 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.
  • 4.
  • 5.
    KnpUniversity.com github.com/weaverryan Who is thisguy? > 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.
    What’s the hardest partof Symfony? @weaverryan
  • 7.
  • 8.
    Authorization Do you haveaccess to do X? @weaverryan
  • 9.
  • 10.
  • 11.
    1) Grab informationfrom the request @weaverryan
  • 12.
    2) Load aUser @weaverryan
  • 13.
    3) Validate ifthe credentials are valid @weaverryan
  • 14.
  • 15.
    5) authentication failure… dang, now what?! @weaverryan
  • 16.
    6) How dowe “ask” the user to login? @weaverryan
  • 17.
    6 Steps 5 DifferentClasses @weaverryan
  • 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.
  • 20.
  • 21.
    interface GuardAuthenticatorInterface
 {
 public functiongetCredentials(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.
  • 23.
    You still haveto do work! @weaverryan and wear sunscreen!
  • 24.
    But it willbe simple @weaverryan https://github.com/knpuniversity/guard-presentation
  • 25.
    You need aUser class (This has nothing to do with Guard) @weaverryan ☼
  • 26.
  • 27.
    class User implementsUserInterface
 {
 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.
    @weaverryan class User implementsUserInterface
 {
 // … 
 public function getPassword()
 {
 }
 public function getSalt()
 {
 }
 public function eraseCredentials()
 {
 }
 } These are only used for users that have an encoded password
  • 29.
    The Hardest ExampleEver: Form Login @weaverryan ☼ ☼
  • 30.
    A traditional loginform setup @weaverryan
  • 31.
    class SecurityController extendsController
 {
 /**
 * @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.
    <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.
  • 34.
    class FormLoginAuthenticator extendsAbstractGuardAuthenticator
 {
 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.
    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.
    public function getUser($credentials,UserProviderInterface $userProvider)
 {
 $username = $credentials['username'];
 
 $user = new User();
 $user->setUsername($username);
 
 return $user;
 } Create/Load that User! @weaverryan
  • 37.
    public function checkCredentials($credentials,UserInterface $user)
 {
 $password = $credentials['password'];
 if ($password == 'santa' || $password == 'elves') {
 return;
 }
 
 return true;
 } Are the credentials correct? @weaverryan
  • 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.
    public function onAuthenticationSuccess(Request$request, TokenInterface $token, $providerKey)
 {
 $url = $this->router->generate('homepage');
 
 return new RedirectResponse($url);
 } Amazing. Auth worked. Now what? @weaverryan
  • 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.
    Register as aservice services:
 form_login_authenticator:
 class: AppBundleSecurityFormLoginAuthenticator
 autowire: true
 @weaverryan
  • 42.
    Activate in yourfirewall security:
 firewalls:
 main:
 anonymous: ~
 logout: ~
 guard:
 authenticators:
 - form_login_authenticator @weaverryan
  • 43.
    User Providers (This hasnothing to do with Guard) @weaverryan ☼☼ ☼
  • 44.
    Each App hasa User class @weaverryan And the Christmas spirit
  • 45.
    Each User ClassNeeds 1 User Provider @weaverryan
  • 46.
    class SunnyUserProvider implementsUserProviderInterface
 {
 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.
  • 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.
    class SunnyUserProvider implementsUserProviderInterface
 {
 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.
    class SunnyUserProvider implementsUserProviderInterface
 {
 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.
    class SunnyUserProvider implementsUserProviderInterface
 {
 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.
    Slightly more sunshiney: Loadinga User from the Database @weaverryan
  • 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.
    class SunnyUserProvider implementsUserProviderInterface
 {
 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.
    Easiest Example ever: Api(JWT) Authentication @weaverryan Warmest
  • 56.
    JSON Web Tokens @weaverryan Q)What if an API client could simply send you its user id as authentication? Authorization: Bearer 123
  • 57.
    1) API clientauthenticates API client Hey dude, I’m weaverryan POST /token username=weaverryan password=I<3php app
  • 58.
    2) Is thisreally weaverryan? API client “It checks out, weaverryan’s password really is I<3php. Nerd” POST /token username=weaverryan password=I<3php app
  • 59.
    3) Create apackage of data API client app $data = [
 'username' => 'weaverryan'
 ];
  • 60.
    4) Sign thedata! $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.
    5) Send thetoken back! API client app {
 "token": "big_long_json_webtoken"
 } POST /token username=weaverryan password=I<3php
  • 62.
    6) Client sendsthe token API client app GET /secret/stuff Authorization: Bearer big_login_json_webtoken
  • 63.
    7) Verify thesignature // "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.
    8) Decode thetoken 
 $payload = $jws->getPayload();
 
 $username = $payload['username'];

  • 65.
  • 66.
    @weaverryan 1) Install alibrary to help sign tokens composer require lexik/jwt-authentication-bundle
  • 67.
    @weaverryan 2) Create apublic & 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.
    @weaverryan 3) Point thelibrary 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.
    4) Endpoint toreturn 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.
    5) Create theJWT Authenticator
  • 71.
    class JwtAuthenticator extendsAbstractGuardAuthenticator
 { 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.
    public function getCredentials(Request$request)
 {
 $extractor = new AuthorizationHeaderTokenExtractor(
 'Bearer',
 'Authorization'
 );
 
 $token = $extractor->extract($request);
 
 if (false === $token) {
 return;
 }
 
 return $token;
 } @weaverryan
  • 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.
    public function checkCredentials($credentials,UserInterface $user)
 {
 // no credentials to check
 return true;
 }
 @weaverryan
  • 75.
    public function onAuthenticationFailure(Request$request, AuthenticationException $exception)
 {
 return new JsonResponse([
 'message' => $exception->getMessageKey()
 ], 401);
 } @weaverryan
  • 76.
    public function onAuthenticationSuccess(Request$request, TokenInterface $token, $providerKey)
 {
 // let the request continue to the controller
 return;
 } @weaverryan
  • 77.
    Register as aservice # app/config/services.yml
 services:
 jwt_authenticator:
 class: AppBundleSecurityJwtAuthenticator
 autowire: true @weaverryan
  • 78.
    Activate in yourfirewall security:
 # ...
 firewalls:
 main:
 # ...
 guard:
 authenticators:
 - form_login_authenticator
 - jwt_authenticator
 entry_point: form_login_authenticator
 which “start” method should be called
  • 79.
    curl http://localhost:8000/secure <!DOCTYPE html> <html> <head> <metacharset="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.
    curl --header “Authorization:Bearer BAD" http://localhost:8000/secure {"message":"Username could not be found."} @weaverryan
  • 81.
    curl --header “Authorization:Bearer GOOD" http://localhost:8000/secure {"message":"Hello from the secureAction!"} @weaverryan
  • 82.
  • 83.
  • 84.
    composer require league/oauth2-facebook knpuniversity/oauth2-client-bundle @weaverryan
  • 85.
    @weaverryan # app/config/config.yml
 knpu_oauth2_client:
 clients:
 # createsservice: "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.
    @weaverryan /**
 * @Route("/connect/facebook", name="connect_facebook")
 */
 publicfunction 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.
    class FacebookAuthenticator extendsAbstractGuardAuthenticator
 {
 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.
    public function getCredentials(Request$request)
 {
 if ($request->getPathInfo() != '/connect/facebook-check') {
 return;
 }
 
 return $this->oAuth2Client->getAccessToken($request);
 } @weaverryan
  • 89.
    public function getUser($credentials,…)
 {
 /** @var AccessToken $accessToken */
 $accessToken = $credentials;
 
 /** @var FacebookUser $facebookUser */
 $facebookUser = $this->oAuth2Client
 ->fetchUserFromToken($accessToken);
 
 // ...
 } @weaverryan
  • 90.
    Now, relax inthe shade! @weaverryan
  • 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.
    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.
    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.
    * also supports“finishing registration” @weaverryan
  • 95.
  • 96.
    Can I controlthe error message? @weaverryan
  • 97.
    @weaverryan If authentication failed,it is because an AuthenticationException (or sub-class) was thrown (This has nothing to do with Guard)
  • 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.
    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.
    How can Icustomize the message? @weaverryan Create a new sub-class of AuthenticationException for each message and override getMessageKey()
  • 101.
  • 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.
    I need tomanually authenticate my user @weaverryan
  • 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.
    I want tosave a lastLoggedInAt field on my user no matter *how* they login @weaverryan
  • 106.
    Chill… that wasalready possible SecurityEvents::INTERACTIVE_LOGIN @weaverryan
  • 107.
    class LastLoginSubscriber implementsEventSubscriberInterface
 {
 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.
    @weaverryan Ok, so howdo I make my weird auth system?
  • 109.
    1. User implementsUserInterface @weaverryan
  • 110.
  • 111.
    3. Create yourauthenticator(s) @weaverryan
  • 112.
  • 113.
  • 114.
    @weaverryan New (free) Symfony3 Tutorial KnpUniversity.com Thank You!