Skip to content

Latest commit

 

History

History
256 lines (200 loc) · 17.9 KB

Exercise_24_MakeYourApplicationSecure.md

File metadata and controls

256 lines (200 loc) · 17.9 KB

Exercise 24: Make your Application Secure

Learning Goal

In the previous exercise you learned how you can protect your application with the application router. But unauthenticated and/or unauthorized requests could be sent directly to your app - bypassing the application router. Hence, the application itself must also ensure that only those requests are served which are sent from an authenticated and authorized user.

After this exercise you will know how to secure your application and introduce (domain specific) authorization checks.

Your task is to secure your application with the SAP Java Container Security library and the Spring Security framework, so that the application blocks all incoming requests if the user is not authenticated or has no authorization for the needed scope "$XSAPPNAME.Display".

Note: There is currently no easy way to make a subset of apps 'unreachable' via http(s) from the outside, e.g. by network segregation. But even if we had that capability, it would still be necessary to have authorization checks in the 'backend' for all sensitive operations.

Prerequisite

Continue with your solution of the last exercise. If this does not work, you can checkout the branch solution-23-Setup-Generic-Authorization.

Step 1: Integrate SAP Java Container Security library

  • Get the current version of the SAP Java Container Security library from SAP Service Marketplace (the filename currently is XS_JAVA_1-70001362.ZIP. The version/filename may change in the future).

    Please note that only SAP customers will be able to download the library from SAP Service Marketplace.

  • Extract the downloaded Zip file.

  • Navigate into unzipped directory.

  • Run the command mvn install.

Add the following dependency to your pom.xml using the XML view of Eclipse:

  • Add the java-container-security dependency. Make sure that the version matches to the one downloaded from the SAP Service Marketplace:
<!-- Security -->
<dependency>
    <groupId>com.sap.xs2.security</groupId>
    <artifactId>java-container-security</artifactId>
    <version>0.27.2</version> 
</dependency>
  • UPDATE(!): when using mvn-install of version > 3.0.0 you need in addition to add the following dependencies:
<!-- we need to add addtional dependencies because mvn install-file does not install bundled pom.xml from artifacts -->
<!-- see here https://issues.apache.org/jira/browse/MINSTALL-110 -->
<!-- BEGIN additional dependencies -->
<dependency>
    <groupId>com.sap.xs2.security</groupId>
    <artifactId>security-commons</artifactId>
    <version>0.27.2</version>
</dependency>
<dependency>
    <groupId>com.sap.xs2.security</groupId>
    <artifactId>java-container-security-api</artifactId>
    <version>0.27.2</version> 
</dependency>
<dependency>
    <groupId>com.sap.security.nw.sso.linuxx86_64.opt</groupId>
    <artifactId>sapjwt.linuxx86_64</artifactId>
    <version>1.0.19</version>
</dependency> 
<dependency> <!-- windows -->
    <groupId>com.sap.security.nw.sso.ntamd64.opt</groupId>
    <artifactId>sapjwt.ntamd64</artifactId>
    <version>1.0.19</version>
</dependency>

<!-- Spring Security and other related libraries-->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.0.8.RELEASE</version>
    <exclusions>
        <exclusion>
            <artifactId>bcpkix-jdk15on</artifactId>
            <groupId>org.bouncycastle</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.0.11.RELEASE</version>
</dependency>

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>
<dependency>
    <groupId>com.unboundid.components</groupId>
    <artifactId>json</artifactId>
    <version>1.0.0</version>
</dependency>
<dependency>
    <groupId>org.cloudfoundry.identity</groupId>
    <artifactId>cloudfoundry-identity-client-lib</artifactId>
    <version>4.7.4</version>
</dependency>
<!-- END additional dependencies -->
  • Note: After you've changed the Maven settings, don't forget to update your Eclipse project (Alt+F5)!

Step 2: Configure Spring Security

Add and modify WebSecurityConfig class

  • Create a WebSecurityConfig class in the package com.sap.bulletinboard.ads.config and copy the code from here.
  • Change the value of field XSAPPNAME from "bulletinboard-d012345" to "bulletinboard-<Your user id>".
    Note: The value of private static final String XSAPPNAME must be equal to the value of xsappname, that is defined in your xs-security.json file.

Technically - under the hood - the default AffirmativeBased AccessDecisionManager is used. This holds the WebExpressionVoter, which in turn makes use of the OAuth2WebSecurityExpressionHandler that handles Spring EL expressions like hasRole or isAuthenticated (read more). If you require more than one Voter you can specify a "custom" AccessDecisionManager such as UnanimousBased.

You have now enabled security centrally on the web level. Besides that you have the option to do the authorization checks on method level using Method Security.

Activate Security by registering springSecurityFilterChain Servlet Filter

  • In order to activate the Spring Security framework you need to add a servlet filter in the AppInitializer.onStartup() method.
// register filter with name "springSecurityFilterChain"
servletContext.addFilter(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME,
                        new DelegatingFilterProxy(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME))
              .addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), false, "/*");

The DelegatingFilterProxy intercepts the requests and adds a ServletFilter chain between the web container and your web application, so that the Spring Security framework can filter out unauthenticated and unauthorized requests.

Step 3: Setup Security for Component Tests

The service tests from Exercise 4 are not affected by the above changes. They are still running even if the configuration in WebSecurityConfig class is loaded into the application context. We strongly recommend you to activate security for your service level tests to ensure automatically that all of your application endpoints are protected against unauthorized access. In this step you will learn to "fake" the security infrastructure, so that the Unit Tests can also test the security settings.

Activate Security

  • Like in the AppInitializer.onStartup() method we also need to make sure, that springSecurityFilterChain bean is added as filter to Mock MVC in the AdvertisementControllerTest test class:
@Inject //use javax.servlet.Filter
private Filter springSecurityFilterChain;

@Before
public void setUp() throws Exception {
        mockMvc = MockMvcBuilders.webAppContextSetup(context).addFilters(springSecurityFilterChain).build();
}    
  • Now run your JUnit tests and see them failing because of unexpected 401 ("unauthenticated") status code.

Fake Test Security Infrastructure

  • Create folder cc-bulletinboard-ads/src/test/resources and copy the files privateKey.txt, publicKey.txt into the new folder.

  • Copy the implementation of the TestSecurityConfig class from here into the test package named com.sap.bulletinboard.ads.config.
    In productive environments, SAPOfflineTokenServicesCloud reads the public key value from the environment variable VCAP_SERVICES. For unit tests, you explicitly set the public key of your test key pair with the JwtGenerator. The JwtGenerator takes the public key from the publicKey.txt file.

  • Copy the implementation of the JwtGenerator class from here into a new test package named com.sap.bulletinboard.ads.testutils.

Step 4: Fix and Run Component Tests

Generate a valid JWT Token

  • Update the setup of the AdvertisementControllerTest test class according to the below code snippet:
private String jwt;

@Before
public void setUp() throws Exception {
    ...
    // compute valid token with Display and Update scopes
    // tenant specific XSAPPNAME (appid) looks like <xsappname>!t<tenant specific index> 
    jwt = new JwtGenerator().getTokenForAuthorizationHeader("bulletinboard-<<your user id>>!t27.Display", "bulletinboard-<<your user id>>!t27.Update"); 
}

Note: The class JwtGenerator has the responsibility to generate a JWT Token for those scopes which are passed to the getTokenForAuthorizationHeader() method as a String array. It returns the token in a format that is suitable for the HTTP Authorization header. The generator signs the JWT Token with its private key (taken from file privateKey.txt).

Add Authorization header to each HTTP request

The class AdvertisementControllerTest must further be updated in those locations where the test performs a HTTP method call. All HTTP method calls must be updated with an HTTP header field of name Authorization and value jwt. For example:

Before... get(AdvertisementController.PATH + "/" + id)

After... get(AdvertisementController.PATH + "/" + id).header(HttpHeaders.AUTHORIZATION, jwt)

Run JUnit tests

Now you can run the JUnit tests as described in Exercise 4. They should succeed now.

Step 5: Run and Test the Service Locally

In this step you prepare the local run environment and test your application manually using Postman to discover that your application is now secure.

Prepare VCAP_SERVICES

Based on the VCAP_SERVICES environment variable the spring-security module instantiates the SecurityContext.

  • In Eclipse, open the Tomcat server settings (by double-clicking on the server) and then open the launch configuration. In the Environment tab edit the VCAP_SERVICES variable and replace the value with the following:
{"postgresql-9.3":[{"name":"postgresql-lite","label":"postgresql-9.3","credentials":{"dbname":"test","hostname":"127.0.0.1","password":"test123!","port":"5432","uri":"postgres://testuser:test123!@localhost:5432/test","username":"testuser"},"tags":["relational","postgresql"],"plan":"free"}],"rabbitmq-lite":[{"credentials":{"hostname":"127.0.0.1","password":"guest","uri":"amqp://guest:[email protected]:5672","username":"guest"},"label":"rabbitmq-lite","tags":["rabbitmq33","rabbitmq","amqp"]}],"xsuaa":[{"credentials":{"clientid":"testClient!t27","clientsecret":"dummy-clientsecret","identityzone":"d012345trial","identityzoneid":"a09a3440-1da8-4082-a89c-3cce186a9b6c","tenantid":"d012345trial","tenantmode":"shared","url":"dummy-url","verificationkey":"-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn5dYHyD/nn/Pl+/W8jNGWHDaNItXqPuEk/hiozcPF+9l3qEgpRZrMx5ya7UjGdvihidGFQ9+efgaaqCLbk+bBsbU5L4WoJK+/t1mgWCiKI0koaAGDsztZsd3Anz4LEi2+NVNdupRq0ScHzweEKzqaa/LgtBi5WwyA5DaD33gbytG9hdFJvggzIN9+DSverHSAtqGUHhwHSU4/mL36xSReyqiKDiVyhf/y6V6eiE0USubTEGaWVUANIteiC+8Ags5UF22QoqMo3ttKnEyFTHpGCXSn+AEO0WMLK1pPavAjPaOyf4cVX8b/PzHsfBPDMK/kNKNEaU5lAXo8dLUbRYquQIDAQAB-----END PUBLIC KEY-----","xsappname":"bulletinboard-d012345"},"label":"xsuaa","name":"uaa-bulletinboard","plan":"application","tags":["xsuaa"]}]}
  • If you run the application from the command line, update your localEnvironmentSetup script accordingly to localEnvironmentSetup.sh (localEnvironmentSetup.bat)
  • In both cases make sure that you've changed the value of field xsappname from "bulletinboard-d012345" to "bulletinboard-<<Your user id>>".

Note: With this configuration we can mock the XSUAA backing service as we make use of so-called "offlineToken verification". Having that we can simulate a valid JWT Token to test our service as described below.

Generate JWT Token

Before calling the service you need to provide a digitally signed JWT token to simulate that you are an authenticated user.

  • Therefore simply set a breakpoint in JWTGenerator.getTokenForAuthorizationHeader() in package com.sap.bulletinboard.ads.testutils and run the JUnit tests again to fetch the value of jwt from there.

Explanation: The generated JWT Token is an "individual one" as it

  • contains specific scope(s) e.g. bulletinboard-<<your user id>>.Display (as defined in your WebSecurityConfig class). Furthermore note that the scope is composed of xsappname e.g. bulletinboard-<<your user id>> which also needs to be the same as provided as part of the VCAP_SERVICES--xsuaa--xsappname
  • it is signed with a private key that fits to the public key that is provided as part of the VCAP_SERVICES--xsuaa--verificationkey

Call local Service

Now you can test the service manually in the browser using the Postman chrome plugin.

  • You should get for any endpoint (except for \health) an 401 ("unauthorized") status code.
  • Add a header field Authorization with the value of the generated JWT token.
  • Then you can check whether you are able to request the /api/v1/ads endpoints. In case your offlineToken verification fails, make sure that the VCAP_SERVICES environment variable is provided on Tomcat as described above, another restart might be required.

Step 6: Deploy and Test

In this step you are going to deploy your application to Cloud Foundry and discover that you are not any longer authorized to call your service endpoints directly. This is due to to fact that the necessary scopes are not (yet) assigned to your user account. Unlike in the previous steps, your application is now running in a productive security environment which enforces the current existing security policy.

Bind UAA Service to your application

Before deploying your application to Cloud Foundry you need to bind your application to the UAA service.

  • As part of the manifest.yml you need to enhance the list of services bound to the bulletinboard-ads application with the name of your XSUAA service:
- name: bulletinboard-ads
  services:
  ...
  - uaa-bulletinboard
  • Now re-deploy your application to Cloud Foundry.

Call deployed service

  • Call your service endpoints e.g. https://bulletinboard-ads-<<your user id>>.cfapps.<<region>>.hana.ondemand.com manually using the Postman Chrome plugin. You should get for any endpoint (except for \health) an 401 ("unauthorized") status code.
  • On Cloud Foundry it is not possible to provide a valid JWT token which is accepted by the XSUAA. Therefore if you like to provoke a 403 ("forbidden", "insufficient_scope") status code you need to call your application via the approuter e.g.
    https://<<your tenant>>-approuter-<<your user id>>.cfapps.<<region>>.hana.ondemand.com/ads/api/v1/ads in order to authenticate yourself and to create a JWT Token with no scopes. BUT you probably will get as response the login screen in HTML. That's why you need to
    • enable the Interceptor within Postman. You might need to install another Postman Interceptor Chrome Plugin, which will help you to send requests using browser cookies through the Postman app.
    • logon via Chrome Browser first and then
    • back in Postman resend the request e.g.
      https://<<your tenant>>-approuter-<<your user id>>.cfapps.<<region>>.hana.ondemand.com/ads/api/v1/ads and
    • make sure that you now get a 403 status code.

Note:
By default the application router enables CSRF protection for any state-changing HTTP method. That means that you need to provide a x-csrf-token: <token> header for state-changing requests. You can obtain the <token> via a GET request with a x-csrf-token: fetch header to the application router.

Further Reading


© 2018 SAP SE

Previous Exercise Next Exercise