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

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)

86db2617dad3a827499c0bc0a253ea63?s=128

Marco Loche

May 17, 2014
Tweet

Transcript

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

    a distributed web application Marco Loche PHPDay 2014
  2. PHP is the language Community gives value

  3. 1. Introduction

  4. Who am I ?

  5. About  my  company  

  6. eLearning •  Learning Management System – Content management system dedicated to

    learning management  
  7. 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  
  8. How to serve content remotly and on different domains keeping

    control of whoever is using it ?
  9. 2. Context

  10. An open system for browsing, sharing and including resources

  11. API era is now

  12. REST is the best

  13. A matter of consuming resources

  14. Uniform  interface   Uniform interface

  15. 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)
  16.     Be fluent and transitional

  17. REST in Symfony jms/serializer-bundle friendsofsymfony/rest-bundle willdurand/hateoas-bundle nelmio/api-doc-bundle

  18. Lesson learned

  19. 3. Managing origin of distributed client

  20. Protecting personal data and identity integrity    

  21.  Same Origin Policy

  22. Pages of the same origin can manipulate each other's DOM

  23. Content loaded from one domain can not interact with content

    coming from another
  24. Extended to other aspects : HTTP Cookies, XMLHttpRequest

  25. Defining origin http://tools.ietf.org/html/rfc6454 trusted === {scheme, host, port} not trusted

    !== {scheme, host, port}
  26. So what’s trustworthy? http://www.example.com/index.html http://www.example.com/content/page2.html http://user:pwd@www.example.com/api/resource

  27. And what’s not? http://www.example.com/index.html http://www.example.com:81/content/page2.html https://www.example.com/content/page2.html http://example.com/content/page2.html http://api.example.com/content/page2.html

  28. We need to relax

  29. Techniques for relaxing   •  document.domain property •  Cross-document messaging

    •  JSONP •  CORS  
  30. document.domain property http://www.example.com/ <script> document.domain = 'example.com'; </script> http://store.example.com/ <script>

    document.domain = 'example.com'; </script>
  31. 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>
  32. JSONP <script type="text/javascript” src="http://api.example.net/resources?callback=handleJsonp”> </script>

  33. JSONP <script type="text/javascript”> handleJsonp({ /** json data from http://api.example.net/resources**/ });

    </script>
  34. 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
  35. Cross-Origin Resource Sharing •  W3C Recomandation (16 january 2014) • 

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

    request Actual response onload / onerror
  37. Simple Request Method HEAD GET POST  

  38. Simple request header Accept Accept-Language Content-Type Content-Language

  39. Simple response header Cache-Control Content-Language Content-Type Expires Last-Modified Pragma

  40. GET /resource HTTP/1.1 Origin: http://www.example.com Host: api.example.com ... HTTP/1.1 200

    OK Access-Control-Allow-Origin: http://www.example.com ...
  41. Cross-Origin Request with Prefilght JavaScript Code Browser API Server xhr.send()

    Actual request Actual response onload / onerror Prefligth request Preflight response
  42. •  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
  43. 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 ...
  44. 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
  45. Implementation Client side: •  Nicolas Zakas’s Request Object Wrapper • 

    jQuery Server side: •  NelmioCorsBundle
  46. composer.json "require": { "nelmio/cors-bundle": "~1.0"}, }

  47. app/config/config.yml nelmio_cors: defaults: allow_credentials: false allow_origin: [*] allow_methods: ['GET','PUT','POST'] expose_headers:

    [] max_age: 3600 hosts: []
  48. app/config/config.yml nelmio_cors: defaults: allow_credentials: false allow_origin: [*] allow_methods: ['GET','PUT','POST'] expose_headers:

    [] max_age: 3600 hosts: []
  49. app/config/config.yml nelmio_cors: defaults: allow_credentials: false allow_origin: [*] allow_methods: ['GET','PUT','POST'] expose_headers:

    [] max_age: 3600 hosts: []
  50. app/config/config.yml nelmio_cors: defaults: allow_credentials: false allow_origin: [*] allow_methods: ['GET','PUT','POST'] expose_headers:

    [] max_age: 3600 hosts: []
  51. […] paths: '^/api/': allow_origin: ['*'] allow_methods: ['GET','PUT','POST'] '^/api/sensitiveResource': allow_origin:['https://trusted.io']

  52. Lesson learned If you want to be RESTfull you have

    to go with CORS
  53. 4. Managing  security  of  your   resources  

  54. Authentication •  Basic authentication •  Digest authentication •  Token authentication

    •  API Key •  WSSE •  OpenID
  55. Authorization •  Authorization token •  OAuth 1.0a •  OAuth 2

  56. API Security in Symfony hwi/HWIOAuthBundle friendsofsymfony/oauth-server-bundle escapestudios/EscapeWSSEAuthenticationBundle

  57. HANDS ON

  58. 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
  59. Custom Authentication Provider

  60. app/configu/security.yml security: providers: [...] capturator: id: capturator.security.tokenAuth in_memory: memory: users:

    authentication-authority: - password: %auth_authority_pwd% - roles: ROLE_AUTH'
  61. 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'
  62. 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 }
  63. 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>
  64. 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>
  65. 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); } }
  66. 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); } }
  67. 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; } }
  68. 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); } }
  69. 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"); } }
  70. The SimplePreAuthenticatorInterface interface was introduced in Symfony 2.4.

  71. Extendig NelmioCorsBundle Associating Origin and Authorization

  72. /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']
  73. /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']
  74. 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>
  75. 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>
  76. Capturator/CorsBundle/Options/CustomerDomainRepositoryInterface.php namespace Capturator\CorsBundle\Options; interface CustomerDomainRepositoryInterface { /** * @param $origin

    * @return mixed */ public function findOneByOriginDomain( $origin, $token);}
  77. 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; } } }
  78. 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; } } }
  79. 5. Conclusion

  80. Work done

  81. Prefer open protocol

  82. Access control

  83. 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/  
  84. 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@ca@on.html  
  85. Thanks http://joind.in/11303 @netamorfose www.marcoloche.com marco@marcoloche.com All pictures are mine

  86. Q & A http://joind.in/11303 @netamorfose www.marcoloche.com marco@marcoloche.com All pictures are

    mine