Sécurité avec Symfony

Sécurité avec Symfony

Présentation faite au SfPot Paris, juillet 2015.

34ade09dd3d11004ca8ee4174fd3d6a2?s=128

Sarah KHALIL

July 23, 2015
Tweet

Transcript

  1. SECURITY WITH SYMFONY Sarah Khalil - @saro0h @catlannister SfPot Paris

    - juillet 2015
  2. WHO AM I? • Head of • Trainer & Developer

    • Enjoying sharer
  3. WHAT’S THE PLAN? 1. Security in Symfony: Authentication + Authorization

    2. Brand new: Guard
  4. I. SECURITY IN SYMFONY

  5. Security Component

  6. Security Bundle

  7. 2 MAIN PARTS •Authentication •Authorization

  8. A. LET’S TALK ABOUT AUTHENTICATION

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

    be.
  10. WHAT IS A USER? The object stores: • credentials •

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

  12. GIMME THAT AUTHENTICATION THING Security = configuration. (most of the

    time)
  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 this 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 | 401: Unauthorized /admin/myprofile or
  77. B. AUTHORIZATION

  78. BEFORE GETTING ANY FURTHER No more security.context!

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

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

  82. 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’);

  83. 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’);

  84. OK, back to business.

  85. $this-­‐>denyAccessUnlessGranted('ROLE_ADMIN',  null,  'Unable  to  access  this  page!');   $decision  =

     $this-­‐>get('security.authorization_checker')-­‐>isGranted('ROLE_ADMIN');   if  (false  ===  $decision)  {          throw  $this-­‐>createAccessDeniedException('Unable  to  access  this  page!');   } What you have usually in a controller or
  86. Or in a template {%  if  is_granted('ROLE_ADMIN')  %}    

         <a  href="...">Delete</a>   {%  endif  %}
  87. Or in a template {%  if  is_granted('ROLE_ADMIN')  %}    

         <a  href="...">Delete</a>   {%  endif  %} Symfony\Bridge\Twig\Extension\SecurityExtension
  88. HOW DOES IT WORKS?

  89. Let’s take a look at the security.authorization_chec ker service.

  90.        final  public  function  isGranted($attributes,  $object  =  null)

             {                  //  Checks  if  there  is  someone  authenticated  (if  there  is  a  token)   //  Makes  sure  that  the  attributes  in  an  array   //  returns  true  or  false                  return  $this-­‐>accessDecisionManager-­‐>decide($token,  $attributes,  $object);          } Symfony\Component\Security\Core\Authorization \AccessDecisionManagerInterface Any string that represent a permission anything that can help to make a decision
  91. Symfony\Component\Security\Core\Authorization \AccessDecisionManager The object needs: • voters • a strategy

    • $allowIfAllAbstainDecisions • $allowIfEqualGrantedDeniedDecisions
  92. VOTERS Voters are added in a compiler pass (Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass )

    The services must have a « security.voter » tag to be added in the AccessDecisionManager.
  93. VOTERS Voters are added in a compiler pass (Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass )

    The services must have a « security.voter » tag to be added in the AccessDecisionManager. All the voters are sorted by the attribute priority (the higher one is the last to be executed). If you don’t put any priority, => 0 (first to be executed) Why? To make sure that is doesn’t mess with other decisions taken
  94. $strategy, $allowIfAllAbstainDecisions, $allowIfEqualGrantedDeniedDecisions //  Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension   $container      

       -­‐>getDefinition('security.access.decision_manager')          -­‐>addArgument($config['access_decision_manager']['strategy'])          -­‐>addArgument(                    $config['access_decision_manager']['allow_if_all_abstain']          )          -­‐>addArgument(                    $config['access_decision_manager']['allow_if_equal_granted_denied']          )   ;
  95. $strategy  =   $allowIfAllAbstainDecisions  =   $allowIfEqualGrantedDeniedDecisions  =  true affirmative

    or consensus or unanimous true or false true or false
  96. $strategy  =  affirmative   $allowIfAllAbstainDecisions  =  false   $allowIfEqualGrantedDeniedDecisions  =

     true Default values
  97. Symfony\Component\Security\Core\Authorization \AccessDecisionManager The object needs: • voters • a strategy

    • $allowIfAllAbstainDecisions • $allowIfEqualGrantedDeniedDecisions
  98. //Symfony\Component\Security\Core\Authorization\AccessDecisionManager   public  function  decide(TokenInterface  $token,  array  $attributes,  $object  =

     null)   {            return   $this-­‐>{$this-­‐>strategy} ($token,  $attributes,  $object);   } decideAffirmative decideConsensus decideUnanimous
  99. decideAffirmative • Donne accès si n’importe lequel des voter dit

    ACCES_GRANTED. • Si tous les voters disent ACCES_ABSTAIN => $allowIfAllAbstainDecision prend la décision. One says yes. default = false
  100. decideConsensus • Donne accès si la majorité (sans les abstentions)

    dit ACCES_GRANTED. • S’il y a autant de ACCES_GRANTED que d’ ACCES_DENIED => $allowIfEqualGrantedDeniedDecision prend la décision • Si tous les voters disent ACCES_ABSTAIN => $allowIfAllAbstainDecision prend la décision. Most of them says yes. default = true
  101. decideUnanimous • Donne accès si tous les voters (sans les

    abstentions) disent ACCES_GRANTED. • Si tous les voters disent ACCES_ABSTAIN => $allowIfAllAbstainDecision prend la décision. All of them says yes. default = false
  102. decideXxxx //Symfony\Component\Security\Core\Authorization\AccessDecisionManager   //…   foreach  ($this-­‐>voters  as  $voter)  {

             $result  =  $voter-­‐>vote($token,  $object,  $attributes);   }   //  logic  to  return  true  or  false   200 or 403 (forbidden)
  103. Voter MUST NOT be called by you. Voter = Private

    Service
  104. Voter MUST NOT be called by you. Voter = Private

    Service
  105. ALL TOGETHER NOW!

  106. AuthorizationChecker::isGranted()

  107. Controller or template or service from AuthorizationChecker::isGranted()

  108. Controller or template or service from AuthorizationChecker::isGranted() AccessDecisionManager::decide()

  109. Controller or template or service from AuthorizationChecker::isGranted() Strategy Voters Decision

    maker if all can’t make a decision Decision maker if all abstain needs AccessDecisionManager::decide()
  110. Controller or template or service from AuthorizationChecker::isGranted() AccessDecisionManager::decideAffirmative() or AccessDecisionManager::decideConsensus()

    or AccessDecisionManager::decideUnanimous() Strategy Voters Decision maker if all can’t make a decision Decision maker if all abstain needs AccessDecisionManager::decide()
  111. Controller or template or service from VoterInterface::vote() … VoterInterface::vote() calls

    AuthorizationChecker::isGranted() AccessDecisionManager::decideAffirmative() or AccessDecisionManager::decideConsensus() or AccessDecisionManager::decideUnanimous() Strategy Voters Decision maker if all can’t make a decision Decision maker if all abstain needs AccessDecisionManager::decide()
  112. Controller or template or service from VoterInterface::vote() … VoterInterface::vote() calls

    AuthorizationChecker::isGranted() AccessDecisionManager::decideAffirmative() or AccessDecisionManager::decideConsensus() or AccessDecisionManager::decideUnanimous() false true returns Strategy Voters Decision maker if all can’t make a decision Decision maker if all abstain needs AccessDecisionManager::decide()
  113. II. BRAND NEW

  114. II. BRAND NEW

  115. None
  116. None
  117. None
  118. Previously… with the SimplePreauthenticator (1/4) • Implement the SimplePreAuthenticatorInterface: 1.createToken(Request

    $request, $providerKey) 2.authenticateToken(TokenInterface $token, UserProviderInterface $userProvider) 3.supportsToken(TokenInterface $token, $provider)
  119. Previously… with the SimplePreauthenticator (1/4) • Implement the SimplePreAuthenticatorInterface: 1.createToken(Request

    $request, $providerKey) 2.authenticateToken(TokenInterface $token, UserProviderInterface $userProvider) 3.supportsToken(TokenInterface $token, $provider) declare it as a service (github_authenticator)
  120. Previously… with the SimplePreauthenticator (2/4) • Implement the UserProviderInterface: 1.loadUserByUsername($username)

    2.refreshUser(UserInterface $user) 3.supportsToken($class)
  121. Previously… with the SimplePreauthenticator (2/4) • Implement the UserProviderInterface: 1.loadUserByUsername($username)

    2.refreshUser(UserInterface $user) 3.supportsToken($class) declare it as a service (github_authenticator)
  122. Previously… with the SimplePreauthenticator (3/4) security: firewalls: #… providers: github_user_provider:

    id: github_user_provider
  123. Previously… with the SimplePreauthenticator (4/4) security: firewalls: secured_area: pattern: ^/admin

    stateless: false simple_preauth: authenticator: github_authenticator provider: github_user_provider logout: path: /admin/logout target: /
  124. None
  125. None
  126. None
  127. Implement the GuardAuthenticatorInterface 1.start(Request $request, AuthenticationException $exception) 2.getCredentials(Request $request) 3.getUser($credentials,

    UserProviderInterface $userProvider) 4.checkCredentials($credentials, UserInterface $user) 5.onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) 6.onAuthenticationFailure(Request $request, AuthenticationException $exception) 7.supportsRememberMe()
  128. LINKS TO REVIEW • http://knpuniversity.com/blog/guard-authentication • https://knpuniversity.com/screencast/guard/api-token#play • https://github.com/knpuniversity/KnpUGuard/tree/master/ src

    • https://github.com/knpuniversity/KnpUGuardBundle • https://github.com/symfony/symfony/pull/14673
  129. • When to create a firewall (different way of authenticating)

    • Voters must be light in case of unanimous or consensus (because of the fact that all voters are executed)
  130. Thank you! @saro0h speakerdeck.com/saro0h/ saro0h github.com/saro0h/oauth-github