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.
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
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
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. @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
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
}
}
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. 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. public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials['username'];
$user = new User();
$user->setUsername($username);
return $user;
}
Create/Load that User!
@weaverryan
36. public function checkCredentials($credentials, UserInterface $user)
{
$password = $credentials['password'];
if ($password == 'santa' || $password == 'elves') {
return;
}
return true;
}
Are the credentials correct?
@weaverryan
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. public function onAuthenticationSuccess(Request $request,
TokenInterface $token, $providerKey)
{
$url = $this->router->generate('homepage');
return new RedirectResponse($url);
}
Amazing. Auth worked. Now what?
@weaverryan
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. Register as a service
services:
form_login_authenticator:
class: AppBundleSecurityFormLoginAuthenticator
arguments: [‘@router’]
@weaverryan
41. Activate in your firewall
security:
firewalls:
main:
anonymous: ~
logout: ~
guard:
authenticators:
- form_login_authenticator
@weaverryan
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';
}
}
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. 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. 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
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. 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!
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. 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()
{
}
}
64. Register as a service
services:
api_token_authenticator:
class: AppBundleSecurityApiTokenAuthenticator
arguments:
- '@doctrine.orm.entity_manager'
@weaverryan
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
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. 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. public function getCredentials(Request $request)
{
if ($request->getPathInfo() != '/connect/facebook-check') {
return;
}
return $request->query->get('code');
}
@weaverryan
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. 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. 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
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. 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. How can I customize the message?
@weaverryan
Create a new sub-class of
AuthenticationException for each message
and override getMessageKey()
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. I need to manually
authenticate my user
@weaverryan
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. I want to save a
lastLoggedInAt
field on my user no
matter *how* they login
@weaverryan
92. Chill… that was already
possible
SecurityEvents::INTERACTIVE_LOGIN
@weaverryan
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. All of these features
are available now!
@weaverryan
Thanks 2.8!