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.
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
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
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
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. @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
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
}
}
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. 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 a service
services:
form_login_authenticator:
class: AppBundleSecurityFormLoginAuthenticator
autowire: true
@weaverryan
42. Activate in your firewall
security:
firewalls:
main:
anonymous: ~
logout: ~
guard:
authenticators:
- form_login_authenticator
@weaverryan
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';
}
}
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. 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. 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
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 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)
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 client authenticates
API client
Hey dude, I’m weaverryan
POST /token
username=weaverryan
password=I<3php
app
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. 3) Create a package of data
API client app
$data = [
'username' => 'weaverryan'
];
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. 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]);
}
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. 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
77. Register as a service
# app/config/services.yml
services:
jwt_authenticator:
class: AppBundleSecurityJwtAuthenticator
autowire: true
@weaverryan
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
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. 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. public function getCredentials(Request $request)
{
if ($request->getPathInfo() != '/connect/facebook-check') {
return;
}
return $this->oAuth2Client->getAccessToken($request);
}
@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
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 I customize the message?
@weaverryan
Create a new sub-class of
AuthenticationException for each message
and override getMessageKey()
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 to manually
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 to save a
lastLoggedInAt
field on my user no
matter *how* they login
@weaverryan
106. Chill… that was already
possible
SecurityEvents::INTERACTIVE_LOGIN
@weaverryan
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