Upgrade to Pro — share decks privately, control downloads, hide ads and more …

DPC 2015 - Implement Single Sign On easily with Symfony

DPC 2015 - Implement Single Sign On easily with Symfony

We'll first make a round trip to the Security world of Symfony2 : understanding what is a provider, a firewall, the two very important services "security.token_storage" and "security.authorization_checker" and of course how the user object is important in Symfony.
Then we'll see how to authenticate a user through a OAuth2 server (like facebook) very easily.

With few lines of code, we'll be able to go further and see how we can make sure that a user stay logged in from application to another.

Sarah KHALIL

June 27, 2015
Tweet

More Decks by Sarah KHALIL

Other Decks in Technology

Transcript

  1. IMPLEMENT SINGLE SIGN ON EASILY WITH SYMFONY Sarah Khalil -

    @saro0h Disclaimer you need to have some experience with security in Symfony.
  2. WHO AM I? • Head of • Trainer & Developer

    • Enjoying sharer • Contributor to
  3. WHAT’S THE PLAN? 1. Security in Symfony 2. Let’s implement

    authentication with Github (without any third party library) 3. Authenticate apps in the SOA context 4. Advices
  4. WHAT DO WE HAVE TO WRITE? Most of the time,

    it’s all about configuration.
  5. WHAT DO WE HAVE TO WRITE? security: encoders: Symfony\Component\Security\Core\User\User: bcrypt

    providers: in_memory: memory: users: sarah: password: $2a$12$LCY0M… roles: 'ROLE_USER' firewalls: admin: provider: in_memory pattern: /^admin form_login: login_path: /login check_path: /login_check
  6. WHAT DO WE HAVE TO WRITE? security: encoders: Symfony\Component\Security\Core\User\User: bcrypt

    providers: in_memory: memory: users: sarah: password: $2a$12$LCY0M… roles: 'ROLE_USER' firewalls: admin: provider: in_memory pattern: /^admin form_login: login_path: /login check_path: /login_check
  7. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); Encoder Factory $factory-­‐>getEncoder($user); $encoder $encoder-­‐>isPasswordValid('mypwd'); Encoder true authenticated false | 401: Unauthorized /admin/myprofile or
  8. Your application all routes begin with /admin all routes beginning

    with /shop all routes beginning with /blog
  9. Your application all routes begin with /admin all routes beginning

    with /shop all routes beginning with /blog all other routes…
  10. Your application all routes begin with /admin all routes beginning

    with /shop all routes beginning with /blog all other routes… The user needs to be authenticated to access that part of the app
  11. WHAT DO WE HAVE TO WRITE? security: firewalls: admin: provider:

    in_memory pattern: /^admin form_login: login_path: /login check_path: /login_check
  12. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider authenticated 401: Unauthorized /admin/myprofile or
  13. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); authenticated 401: Unauthorized /admin/myprofile or
  14. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); authenticated 401: Unauthorized /admin/myprofile or
  15. interface UserProviderInterface { /** * Loads the user for the

    given username. * * This method must throw UsernameNotFoundException if the user is not * found. */ public function loadUserByUsername($username); /** * Refreshes the user for the account interface. * * It is up to the implementation to decide if the user data should be * totally reloaded (e.g. from the database), or if the UserInterface * object can just be merged into some internal array of users / identity * map. */ public function refreshUser(UserInterface $user); /** * Whether this provider supports the given user class. */ public function supportsClass($class); }
  16. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    authenticated Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); /admin/myprofile
  17. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Encoder Factory authenticated Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); /admin/myprofile
  18. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Encoder Factory $factory-­‐>getEncoder($user); authenticated Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); /admin/myprofile
  19. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Encoder Factory $factory-­‐>getEncoder($user); $encoder authenticated Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); /admin/myprofile
  20. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Encoder Factory $factory-­‐>getEncoder($user); $encoder Encoder authenticated Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); /admin/myprofile
  21. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Encoder Factory $factory-­‐>getEncoder($user); $encoder Encoder $encoder-­‐>isPasswordValid('mypwd'); authenticated Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); /admin/myprofile
  22. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Encoder Factory $factory-­‐>getEncoder($user); $encoder Encoder $encoder-­‐>isPasswordValid('mypwd'); authenticated true Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); /admin/myprofile
  23. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Encoder Factory $factory-­‐>getEncoder($user); $encoder Encoder $encoder-­‐>isPasswordValid('mypwd'); authenticated true Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); /admin/myprofile false | 401: Unauthorized or
  24. interface PasswordEncoderInterface { /** * Encodes the raw password. */

    public function encodePassword($raw, $salt); /** * Checks a raw password against an encoded password. */ public function isPasswordValid($encoded, $raw, $salt); }
  25. security: encoders: Symfony\Component\Security\Core\User\User: bcrypt providers: in_memory: memory: users: sarah: password:

    $2a$12$LCY0M… roles: 'ROLE_USER' firewalls: admin: provider: in_memory pattern: /^admin form_login: login_path: /login check_path: /login_check
  26. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); /admin/myprofile
  27. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); /admin/myprofile
  28. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); Encoder Factory /admin/myprofile
  29. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); Encoder Factory $factory-­‐>getEncoder($user); /admin/myprofile
  30. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); Encoder Factory $factory-­‐>getEncoder($user); $encoder /admin/myprofile
  31. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); Encoder Factory $factory-­‐>getEncoder($user); $encoder Encoder /admin/myprofile
  32. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); Encoder Factory $factory-­‐>getEncoder($user); $encoder Encoder $encoder-­‐>isPasswordValid('mypwd'); /admin/myprofile
  33. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); Encoder Factory $factory-­‐>getEncoder($user); $encoder Encoder $encoder-­‐>isPasswordValid('mypwd'); true /admin/myprofile
  34. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); Encoder Factory $factory-­‐>getEncoder($user); $encoder Encoder $encoder-­‐>isPasswordValid('mypwd'); true authenticated /admin/myprofile
  35. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); Encoder Factory $factory-­‐>getEncoder($user); $encoder Encoder $encoder-­‐>isPasswordValid('mypwd'); true authenticated /admin/myprofile or
  36. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); Encoder Factory $factory-­‐>getEncoder($user); $encoder Encoder $encoder-­‐>isPasswordValid('mypwd'); true authenticated false | /admin/myprofile or
  37. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); Encoder Factory $factory-­‐>getEncoder($user); $encoder Encoder $encoder-­‐>isPasswordValid('mypwd'); true authenticated false | 401: Unauthorized /admin/myprofile or
  38. CREATE THE TEMPLATES {% extends 'base.html.twig' %} {% block body

    %} <h1>Homepage !</h1> <p class="lead">This application shows how to implement an authentication with Github with Symfony.</p> {% endblock %} {% extends 'base.html.twig' %} {% block body %} <h1>Administration</h1> <p class="lead">If you can access this page, it's because you are authenticated.</p> <p class="lead"> Actually you are! <br /> Your login: {{ app.user.username }} <br /> Your name (in case you forgot…): {{ app.user.name }} <br /> Aaaaand your email address: {{ app.user.email }} <br /> Your FACE: <img width="100px" src="{{ app.user.avatar }}" </p> {% endblock %} admin.html.twig index.html.twig
  39. security: firewalls: secured_area: pattern: ^/admin stateless: false simple_preauth: authenticator: github_authenticator

    provider: github_user_provider logout: path: /admin/logout target: / Trigger authentication!
  40. security: firewalls: secured_area: pattern: ^/admin stateless: false simple_preauth: authenticator: github_authenticator

    provider: github_user_provider logout: path: /admin/logout target: / Create that authenticator Trigger authentication!
  41. security: firewalls: secured_area: pattern: ^/admin stateless: false simple_preauth: authenticator: github_authenticator

    provider: github_user_provider logout: path: /admin/logout target: / Create that authenticator Create that provider Trigger authentication!
  42. class GithubAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface { public function createToken(Request $request,

    $providerKey) { // This method is called first. // Get an access token to be able to use the API of Github // return a PreAuthenticatedToken storing this access token. No user yet! } public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) { // This method is called secondly. // Call the loadUserByUsername from the provider, responsible of getting the user through the API thanks to the access token // return a PreAuthenticatedToken storing the user and its roles. } public function supportsToken(TokenInterface $token, $providerKey) { // Make sure that the token is an instance of PreAuthenticatedToken // and that it stores the provider key } public function onAuthenticationFailure(Request $request, AuthenticationException $exception) { return new Response("Authentication Failed :(", 401); } }
  43. class GithubUserProvider implements UserProviderInterface { public function loadUserByUsername($username) { //

    Calls the Github API to get the user info. // Creates the User object with all of the info. } public function refreshUser(UserInterface $user) { // Called at each request. // Returns the object user coming from the session. } public function supportsClass($class) { return 'AppBundle\Model\User' === $class; } }
  44. • UserAPI • Contacts the Oauth server; • Is your

    own Oauth server; • Authenticates each request. • Each app must have its OAUTH application credentials registered to have the right settings for each micro service.
  45. Front 1 Front 2 GET / HTTP/1.1 Host: backend.domain.name Authorization:

    bearer xxxx-xxxx GET /#xxxx-xxxx HTTP/1.1 Host: domain.name
  46. Front 1 Front 2 GET / HTTP/1.1 Host: backend.domain.name Authorization:

    bearer xxxx-xxxx GET /#xxxx-xxxx HTTP/1.1 Host: domain.name Event[Listener|Subscriber] => kernel.request to extract the token: 1. Validate the token; 2. Call the API to get the user info from the Oauth server; 3. User authenticated.
  47. Front 1 Front 2 GET / HTTP/1.1 Host: backend.domain.name Authorization:

    bearer xxxx-xxxx GET /#xxxx-xxxx HTTP/1.1 Host: domain.name Event[Listener|Subscriber] => kernel.request to extract the token: 1. Validate the token; 2. Call the API to get the user info from the Oauth server; 3. User authenticated. Is he/she? For Symfony, nope.
  48. Front 1 Front 2 GET / HTTP/1.1 Host: backend.domain.name Authorization:

    bearer xxxx-xxxx GET /#xxxx-xxxx HTTP/1.1 Host: domain.name
  49. Front 1 Front 2 GET / HTTP/1.1 Host: backend.domain.name Authorization:

    bearer xxxx-xxxx GET /#xxxx-xxxx HTTP/1.1 Host: domain.name In the application you are redirecting your user, make sure that the firewall triggers authentication when the user comes from another application (another firewall for another pattern for instance). And in your authenticator extract the access token from your header/URI to finally do your business with the Oauth server to get the user’s info.
  50. Front 1 Front 2 GET / HTTP/1.1 Host: backend.domain.name Authorization:

    bearer xxxx-xxxx GET /#xxxx-xxxx HTTP/1.1 Host: domain.name In the application you are redirecting your user, make sure that the firewall triggers authentication when the user comes from another application (another firewall for another pattern for instance). And in your authenticator extract the access token from your header/URI to finally do your business with the Oauth server to get the user’s info. Now you’re using the security process of Symfony.
  51. IF MULTIPLE FRONTENDS • Send the access token from fronts

    to fronts. • Use: • either the « # » as it is not logged anywhere • and / or the access token in the header. • Use a special listener to get it and have it in the request
  52. • To mutualise the code to access to the Oauth

    server • Authenticator, Provider and User class can be in a shared bundle. • To avoid to much connections to the Oauth server (performances) • Cache strategy!
  53. CACHE STRATEGY In the micro service in charge of the

    authentication : • When getting back the access token, from the URL or the header • check that the Doctrine cache has the access token and until when the token is valid; • if not: • call the Oauth server, get a new access token; • save the access token, when it expires and the user info.
  54. CACHE STRATEGY Store those tokens anywhere you want: Redis, MongoDB…

    Be careful: once logout, the access token needs to be revoked everywhere.