Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Add the security layer to your REST API and ser...

Add the security layer to your REST API and serve a distributed web application

In the past editions of the PhpDay we have assisted to several talks about REST APIs and we learned how to implement a proper REST API service. In this talk I want to present how, at Capturator S.r.l., we have added a security layer to our private REST API (based on Symfony 2) adding authentication token and the support for CORS.

I will start with some theoretical and historical facts on Same Origin Policy and I will present the different solutions to deal with it, dwelling on CORS and its W3C recommendation document. CORS is the solution for a web app that needs to communicate with a REST API able to manage all the CRUD verbs in a distributed architecture (different domain or subdomain).
In the second part I will illustrate the actual implementation of the RESTfull API able to manage distributed and authenticated clients. The backend relies on a couple of Symfony 2 useful bundles and a customization of the security layer.
Presentation given at phpDay 2014 (http://2014.phpday.it)

Marco Loche

May 17, 2014
Tweet

More Decks by Marco Loche

Other Decks in Programming

Transcript

  1. Add the security layer to your (RESTfull) API and serve

    a distributed web application Marco Loche PHPDay 2014
  2. eLearning Sharable Content Object Reference Model – SCORM compliant content must

    be transferable on LMS server – SCORM compliant content can comunicate with LMS – SCORM compliant conten must be agnostic about server technology  
  3. Richardson Model Access through HTTP (RMM level 0) Endpoints of

    the api are resource representations (RMM level 1) Interact with resources with HTTP Verbs and Content negotiation (RMM level 2) … Hypermedia and the glory (RMM level 3)
  4. Cross-document messaging http://www.example.com/ http://store.example.net/ http://example.com/ <script> otherWindow.postMessage("hello there!", "http://example.net"); </script>

    http://example.net/ <script> window.addEventListener("message",receiveMessage,false); function receiveMessage(event) { if (event.origin !== "http://example.com") // DO STUFF HERE WITH event.data } </script>
  5. Ok, but we want more! Previous solutions are valid only

    if you want to be in RMM1 No Verbs No Content Negotiation No Headers for security
  6. Cross-Origin Resource Sharing •  W3C Recomandation (16 january 2014) • 

    Enable true cross domain communication for JavaScript application •  Build on top of the XHR
  7. Cross-Origin Request with Prefilght JavaScript Code Browser API Server xhr.send()

    Actual request Actual response onload / onerror Prefligth request Preflight response
  8. •  CORS with preflight request are originated by : – 

    Other HTTP verbs (PUT PATCH DELETE) –  Other Content-Type (Application/Json) –  Custom Headers (Allow-Header : X-AUTH) –  Non simple header expose (Expose-Header) •  Preflight request + actual request
  9. OPTIONS /resource HTTP/1.1 Origin: http://www.example.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host:

    api.example.com HTTP/1.1 200 OK Access-Control-Allow-Origin: http://www.example.com Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE Access-Control-Allow-Headers: X-Custom-Header Access-Control-Max-Age: 3600 ...
  10. PUT /cors HTTP/1.1 Origin: http://www.example.com Host: api.example.com X-Custom-Header: value  

    HTTP/1.1 200 OK Access-Control-Allow-Origin: http://www.example.com Content-Type: text/html; charset=utf-8
  11. https://www.university.edu 0. authentication Web Application Client LMS SCORM 1. API

    Key https://auth.example.com 2 request authorization https://content.example.com 3 3 send token 4 access resources
  12. app/configu/security.yml security: providers: [...] capturator: id: capturator.security.tokenAuth in_memory: memory: users:

    authentication-authority: - password: %auth_authority_pwd% - roles: 'ROLE_AUTHENTICATION_AUTHORITY'
  13. firewalls: capturator_api_licenser: pattern: ^/api/token(.*) anonymous: ~ http_basic: realm: "Secured Authentication

    Authority Realm" provider: in_memory capturator_api: pattern: ^/api/(.*) capturator: true stateless: true [...] access_control: [...] - { path: ^/api/token(.*), roles: ROLE_AUTH, ip: 192.168.0.1, requires_channel: https }
  14. Capturator/CoreBundle/Resources/config/services.xml <container … > <parameters> <parameter key="capturator.security.userProvider.class"> Capturator\CoreBundle\Security\User\CapturatorUserProvider</parameter> <parameter key="capturator.security.tokenRetriever.class">

    Capturator\CoreBundle\Security\Service\TokenRetriever</parameter> [...] </parameters> <services> <service id="capturator.security.tokenAuth" class="%capturator.security.userProvider.class%"> <argument type="service" id="doctrine.orm.entity_manager" /> </service> [...] </container>
  15. Capturator/CoreBundle/Resources/config/services.xml <container … > <parameters> <parameter key="capturator.security.userProvider.class"> Capturator\CoreBundle\Security\User\CapturatorUserProvider</parameter> <parameter key="capturator.security.tokenRetriever.class">

    Capturator\CoreBundle\Security\Service\TokenRetriever</parameter> [...] </parameters> <services> <service id="capturator.security.tokenAuth" class="%capturator.security.userProvider.class%"> <argument type="service" id="doctrine.orm.entity_manager" /> </service> [...] </container>
  16. class CapturatorListener implements ListenerInterface { protected $securityContext; protected $authenticationManager; protected

    $tokeRetriever; […] public function handle(GetResponseEvent $event) { $request = $event->getRequest(); $requestTokenString = $this->tokeRetriever->retrieve($request); $token = new CapturatorToken(); $token->setUser($requestTokenString); $authToken = $this->authenticationManager->authenticate($token); $this->securityContext->setToken($authToken); } }
  17. class CapturatorListener implements ListenerInterface { protected $securityContext; protected $authenticationManager; protected

    $tokeRetriever; […] public function handle(GetResponseEvent $event) { $request = $event->getRequest(); $requestTokenString = $this->tokeRetriever->retrieve($request); $token = new CapturatorToken(); $token->setUser($requestTokenString); $authToken = $this->authenticationManager->authenticate($token); $this->securityContext->setToken($authToken); } }
  18. namespace Capturator\CoreBundle\Security\Service; use Symfony\Component\HttpFoundation\Request; use Capturator\CoreBundle\Exception\AuthenticationException; class TokenRetriever implements TokenRetrieverInterface

    { public function retrieve(Request $request) { if (!$request->query->has('token') && !$request->headers->has('x-cassys-auth')) { throw new AuthenticationException( ‘Missing required authentication parameter', __CLASS__); } $requestTokenString = $request->query->has('token') ? $request->query->get('token') : $request->headers->get('x-cassys-auth'); return $requestTokenString; } }
  19. class CapturatorListener implements ListenerInterface { protected $securityContext; protected $authenticationManager; protected

    $tokeRetriever; […] public function handle(GetResponseEvent $event) { $request = $event->getRequest(); $requestTokenString = $this->tokeRetriever->retrieve($request); $token = new CapturatorToken(); $token->setUser($requestTokenString); $authToken = $this->authenticationManager->authenticate($token); $this->securityContext->setToken($authToken); } }
  20. Capturator/CoreBundle/Security/User/CapturatorUserProvider.php class CapturatorUserProvider implements UserProviderInterface { protected $em; public function

    __construct(EntityManager $em) { $this->em = $em; } public function loadUserByUsername( $token) [...] public function refreshUser(UserInterface $user ) [...] public function supportsClass($class) { return ($class == "Capturator\CoreBundle\Security\User\CapturatorUser"); } }
  21. /Users/marco/Progetti/Cassys-Server/app/config/config.yml capturator_cors: allow_origin_entity: CapturatorCoreBundle:CustomerDomainName defaults: allow_credentials: false expose_headers: [] max_age:

    0 hosts: [] paths: '^/api/3/': allow_headers: ['X-Cassys-Auth', 'X-Requested-With', 'Content-Type'] allow_methods: ['POST', 'PUT', 'GET']
  22. /Users/marco/Progetti/Cassys-Server/app/config/config.yml capturator_cors: allow_origin_entity: CapturatorCoreBundle:CustomerDomainName defaults: allow_credentials: false expose_headers: [] max_age:

    0 hosts: [] paths: '^/api/3/': allow_headers: ['X-Cassys-Auth', 'X-Requested-With', 'Content-Type'] allow_methods: ['POST', 'PUT', 'GET']
  23. Capturator/CoreBundle/Resources/config/services.xml <container [...]> <parameters> <parameter key="capturator_cors.customer_origin_provider"> Capturator\CorsBundle\Options\CustomerDomainProvider</parameter> </parameters> <services> <service

    id="capturator.cors_options_provider" class="%capturator_cors.customer_origin_provider%"> <argument>%capturator_cors.map%</argument> <argument>%capturator_cors.defaults%</argument> <argument>%capturator_cors.allow_origin_entity%</argument> <argument type="service" id="doctrine.orm.entity_manager" /> <tag name="nelmio_cors.options_provider" priority="10" /> </service> </services> </container>
  24. Capturator/CoreBundle/Resources/config/services.xml <container [...]> <parameters> <parameter key="capturator_cors.customer_origin_provider"> Capturator\CorsBundle\Options\CustomerDomainProvider</parameter> </parameters> <services> <service

    id="capturator.cors_options_provider" class="%capturator_cors.customer_origin_provider%"> <argument>%capturator_cors.map%</argument> <argument>%capturator_cors.defaults%</argument> <argument>%capturator_cors.allow_origin_entity%</argument> <argument type="service" id="doctrine.orm.entity_manager" /> <tag name="nelmio_cors.options_provider" priority="10" /> </service> </services> </container>
  25. CorsBundle/Options/CustomerDomainProvider.php [...]use Nelmio\CorsBundle\Options\ProviderInterface; class CustomerDomainProvider implements ProviderInterface { [...] public

    function getOptions(Request $request) { [...] $origin = $request->headers->get('Origin'); $token= $request->headers->get('X-CUSTOM-AUTH'); if (!is_null($this->repository->findOneByOriginDomain($origin, $token))) { $options['allow_origin'] = array($origin); $options= array_merge($this->defaults, $options); return $options; } } }
  26. CorsBundle/Options/CustomerDomainProvider.php [...]use Nelmio\CorsBundle\Options\ProviderInterface; class CustomerDomainProvider implements ProviderInterface { [...] public

    function getOptions(Request $request) { [...] $origin = $request->headers->get('Origin'); $token= $request->headers->get('X-CUSTOM-AUTH'); if (!is_null($this->repository->findOneByOriginDomain($origin, $token))) { $options['allow_origin'] = array($origin); $options= array_merge($this->defaults, $options); return $options; } } }
  27. References h7p://www.ics.uci.edu/~fielding/pubs/disserta@on/top.htm   h7ps://www.owasp.org/index.php/REST_Security_Cheat_Sheet#Introduc@on   h7p://www.w3.org/TR/cors/   h7p://www.iana.org/assignments/link-­‐rela@ons/   h7ps://www.oasis-­‐open.org/commi7ees/wss/documents/WSS-­‐Username-­‐02-­‐0223-­‐merged.pdf

      h7p://oauth.net/   h7p://www.ieP.org/rfc/rfc2617.txt   h7p://www.ieP.org/rfc/rfc5849.txt  (OAuth  1.0a)   h7p://www.ieP.org/rfc/rfc6749.txt  (OAutn  2.0)   h7p://tools.ieP.org/html/dra[-­‐kelly-­‐json-­‐hal-­‐06   h7p://www.w3.org/TR/json-­‐ld/  
  28. Readings h7p://en.wikipedia.org/wiki/Cross-­‐origin_resource_sharing   h7ps://code.google.com/p/browsersec/wiki/Part2#Standard_browser_security_features   h7p://www.html5rocks.com/en/tutorials/cors/   h7p://enable-­‐cors.org/   h7p://williamdurand.fr/2012/08/02/rest-­‐apis-­‐with-­‐symfony2-­‐the-­‐right-­‐way/

      h7p://welcometothebundle.com/symfony2-­‐rest-­‐api-­‐the-­‐best-­‐2013-­‐way/   h7p://2012.phpday.it/talk/designing-­‐h7p-­‐interfaces-­‐and-­‐resPul-­‐web-­‐services/   h7p://2013.phpday.it/talk/rest-­‐apis-­‐made-­‐easy-­‐with-­‐symfony2/   h7p://www.slideshare.net/dlondero/rest-­‐in-­‐prac@ce-­‐27335543   h7p://edu.williamdurand.fr/web-­‐security-­‐101-­‐slides/#/   h7p://edu.williamdurand.fr/security-­‐slides/#slide1   h7p://friendsofsymfony.github.io/slides/res@ng-­‐with-­‐symfony2.html#/   h7p://symfony.com/doc/current/cookbook/security/custom_authen@ca@on_provider.html   h7p://symfony.com/doc/current/cookbook/security/api_key_authen@[email protected]