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.

34ade09dd3d11004ca8ee4174fd3d6a2?s=128

Sarah KHALIL

June 27, 2015
Tweet

Transcript

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

    @saro0h
  2. IMPLEMENT SINGLE SIGN ON EASILY WITH SYMFONY Sarah Khalil -

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

    • Enjoying sharer • Contributor to
  4. None
  5. 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
  6. I. SECURITY IN SYMFONY

  7. Security Component

  8. Security Bundle

  9. 2 MAIN PARTS •Authentication •Authorization We won’t talk about that

    today. OK, just a little.
  10. LET’S TALK ABOUT AUTHENTICATION

  11. AUTHENTICATION Ensures that the user is who he claims to

    be.
  12. WHAT DO WE HAVE TO WRITE? Most of the time,

    it’s all about configuration.
  13. WHAT DO WE HAVE TO WRITE?

  14. 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
  15. 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
  16. 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
  17. CONFIGURATION • Firewall • Encoder • Provider

  18. FIREWALL

  19. Determines whether or not the user needs to be authenticated.

    AUTHENTICATION: FIREWALL
  20. Your application

  21. Your application all routes beginning with /shop

  22. Your application all routes begin with /admin all routes beginning

    with /shop
  23. Your application all routes begin with /admin all routes beginning

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

    with /shop all routes beginning with /blog all other routes…
  25. 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
  26. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    authenticated 401: Unauthorized or /admin/myprofile
  27. WHAT DO WE HAVE TO WRITE? security: firewalls: admin: provider:

    in_memory pattern: /^admin form_login: login_path: /login check_path: /login_check
  28. BUILT-IN WAYS OF AUTHENTICATING A USER

  29. http-basic http-digest BUILT-IN WAYS OF AUTHENTICATING A USER

  30. http-basic http-digest form-based BUILT-IN WAYS OF AUTHENTICATING A USER

  31. http-basic http-digest form-based x.509 certificate BUILT-IN WAYS OF AUTHENTICATING A

    USER
  32. PROVIDER

  33. Finds and/or creates users AUTHENTICATION: PROVIDER

  34. Finds and/or creates users AUTHENTICATION: PROVIDER

  35. Finds and/or creates users AUTHENTICATION: PROVIDER

  36. Finds and/or creates users AUTHENTICATION: PROVIDER

  37. Finds and/or creates users AUTHENTICATION: PROVIDER

  38. Finds and/or creates users AUTHENTICATION: PROVIDER

  39. Finds and/or creates users AUTHENTICATION: PROVIDER

  40. Finds and/or creates users AUTHENTICATION: PROVIDER

  41. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

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

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); authenticated 401: Unauthorized /admin/myprofile or
  43. 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
  44. security: providers: in_memory: memory: users: sarah: password: $2a$12$LCY0M… roles: 'ROLE_USER'

  45. 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); }
  46. ENCODER

  47. AUTHENTICATION: ENCODER Used for hashing and comparing a password.

  48. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

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

    Encoder Factory authenticated Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); /admin/myprofile
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. CONFIGURATION security: encoders: Symfony\Component\Security\Core\User\User: bcrypt SecurityBundle

  57. 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); }
  58. ALL TOGETHER NOW!

  59. 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
  60. AUTHENTICATION FLOW backend

  61. AUTHENTICATION FLOW backend /admin/myprofile

  62. AUTHENTICATION FLOW Firewall backend /admin/myprofile

  63. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    /admin/myprofile
  64. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    /admin/myprofile
  65. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

    Provider /admin/myprofile
  66. AUTHENTICATION FLOW Firewall backend username = ‘sarah.khalil’ password = ‘mypwd’

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

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

    Provider $provider-­‐>loadUserByUsername('sarah.khalil'); $user  =  new  User(‘sarah.khalil’); Encoder Factory /admin/myprofile
  69. 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
  70. 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
  71. 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
  72. 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
  73. 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
  74. 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
  75. 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
  76. 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
  77. 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
  78. WHAT IS A USER? The object stores: • credentials •

    associated roles
  79. WHAT IS A USER? Symfony\Component\Security\Core\User\UserInterface Symfony\Component\Security\Core\User\AdvancedUserInterface

  80. WHAT DO WE HAVE MORE? Authorization

  81. SERVICES No more security.context!

  82. None
  83. $this->get('security.context')->getToken()->getUser(); Before 2.6

  84. After $this->get('security.token_storage')->getToken()->getUser(); $this->get('security.context')->getToken()->getUser(); Before 2.6

  85. After $this->get('security.token_storage')->getToken()->getUser(); $this->get('security.context')->getToken()->getUser(); Before 2.6 Before 2.6 $this->get('security.context')->isGranted(‘ROLE_USER’);

  86. After $this->get('security.token_storage')->getToken()->getUser(); $this->get('security.context')->getToken()->getUser(); Before 2.6 Before 2.6 $this->get('security.context')->isGranted(‘ROLE_USER’); After $this->get('security.authorization_checker')->isGranted(‘ROLE_USER’);

  87. II. AUTHENTICATION WITH GITHUB

  88. VERY FIRST STEPS • http://symfony.com/doc/current/cookbook/security/ api_key_authentication.html • https://developer.github.com/v3/oauth/ • https://github.com/settings/applications/new

    • https://github.com/csarrazi/CsaGuzzleBundle • [supports last version of Guzzle]
  89. OAUTH • Protocol • 2 versions • Read the documentation

    of the service you need to use
  90. None
  91. None
  92. None
  93. IMPLEMENTATION

  94. CREATE THE USER CLASS Stores all information coming from the

    provider.
  95. CREATE THE TEMPLATES

  96. 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
  97. CONFIGURE THE SecurityBundle

  98. None
  99. security: firewalls: secured_area: pattern: ^/admin stateless: false simple_preauth: authenticator: github_authenticator

    provider: github_user_provider logout: path: /admin/logout target: /
  100. security: firewalls: secured_area: pattern: ^/admin stateless: false simple_preauth: authenticator: github_authenticator

    provider: github_user_provider logout: path: /admin/logout target: / Trigger authentication!
  101. 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!
  102. 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!
  103. AUTHENTICATOR

  104. 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); } }
  105. Make it a service. id => github_authenticator

  106. PROVIDER

  107. security: firewalls: #… providers: github_user_provider: id: github_user_provider

  108. 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; } }
  109. Make it a service. id => github_user_provider

  110. None
  111. None
  112. III. AUTHENTICATE WITH ALL APPS

  113. OAuth server app 1 app 2 app 3 app n

  114. OAuth server ecommerce API Blog API Frontend Backend

  115. OAuth server ecommerce API Blog API Frontend Backend http://e-commerce.api.domain.name/ http://blog.api.domain.name/

    http://domain.name/ http://domain.name/
  116. • 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.
  117. OAuth server Frontend blog API

  118. OAuth server Frontend blog API http://domain.name/articles http://api-blog.domain.name/articles http://api-user.domain.name/me

  119. OAuth server Frontend blog API http://domain.name/articles http://api-blog.domain.name/articles http://api-user.domain.name/me

  120. OAuth server Frontend blog API http://domain.name/articles http://api-blog.domain.name/articles http://api-user.domain.name/me authenticate

  121. OAuth server Frontend blog API http://domain.name/articles http://api-blog.domain.name/articles http://api-user.domain.name/me authenticate returns

    access token
  122. OAuth server Frontend blog API http://domain.name/articles http://api-blog.domain.name/articles http://api-user.domain.name/me authenticate returns

    access token with access token
  123. OAuth server Frontend blog API http://domain.name/articles http://api-blog.domain.name/articles http://api-user.domain.name/me authenticate returns

    access token validate access token with access token
  124. OAuth server Frontend blog API http://domain.name/articles http://api-blog.domain.name/articles http://api-user.domain.name/me authenticate returns

    access token validate access token validated with access token
  125. OAuth server Frontend blog API http://domain.name/articles http://api-blog.domain.name/articles http://api-user.domain.name/me authenticate returns

    access token validate access token validated get articles with access token
  126. OAuth server Frontend blog API http://domain.name/articles http://api-blog.domain.name/articles http://api-user.domain.name/me authenticate returns

    access token validate access token validated get articles with access token
  127. EXCHANGE TOKEN BETWEEN APPLICATIONS

  128. 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
  129. 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.
  130. 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.
  131. 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
  132. 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.
  133. 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.
  134. 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
  135. IV. ADVICES

  136. • 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!
  137. None
  138. 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.
  139. CACHE STRATEGY Store those tokens anywhere you want: Redis, MongoDB…

    Be careful: once logout, the access token needs to be revoked everywhere.
  140. None
  141. None
  142. Need training? Come see us at the SensioLabs booth!

  143. Thank you! @saro0h speakerdeck.com/saro0h/ saro0h This is a zero guys!

    github.com/saro0h/oauth-github