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.
Continue with your solution of the last exercise. If this does not work, you can checkout the branch solution-23-Setup-Generic-Authorization-Spring5
.
The SAP Java Cloud Security library is available here
To use it add the following dependency to your pom.xml
using the XML view of Eclipse:
<!-- Security -->
<dependency>
<groupId>com.sap.cloud.security.xsuaa</groupId>
<artifactId>spring-xsuaa</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>com.sap.cloud.security</groupId>
<artifactId>java-security-test</artifactId>
<version>2.6.0/version>
<scope>test</scope>
</dependency>
The second dependency adds functionality that we later use for testing security features.
You also need these additional spring dependencies:
<!-- Spring Security and other related libraries-->
<dependency> <!-- includes spring-security-oauth2 -->
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
<version>5.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
<version>5.2.4.RELEASE</version>
</dependency>
<!-- END additional dependencies -->
- Note: After you've changed the Maven settings, don't forget to update your Eclipse project (
Alt+F5
)!
Create a WebSecurityConfig
class in the package com.sap.bulletinboard.ads.config
and copy the code from here.
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.
- 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.
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.
- Like in the
AppInitializer.onStartup()
method we also need to make sure, thatspringSecurityFilterChain
bean is added as filter to Mock MVC in theAdvertisementControllerTest
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.
In productive environments, the security library reads the public key value from the environment variable VCAP_SERVICES
. For unit tests, you can explicitly set the
public key of your test key pair using the SecurityTestRule
from java-security-test.
The SecurityTestRule
takes the public key file from the resources. Since you included
the java security test library, a public/private key pair is already put into the resources and accessible from the resources path /publicKey.txt
and /privateKey.txt
.
- Add the
SecurityTestRule
and ajwt
field to the test class:
@ClassRule
public static SecurityTestRule securityTestRule = SecurityTestRule.getInstance(Service.XSUAA)
.setKeys("/publicKey.txt", "/privateKey.txt");
private String jwt;
- Update the setup of the
AdvertisementControllerTest
test class according to the below code snippet:
@Before
public void setUp() throws Exception {
...
jwt = "Bearer " + securityTestRule.getPreconfiguredJwtGenerator()
.withLocalScopes(WebSecurityConfig.DISPLAY_SCOPE_LOCAL, WebSecurityConfig.UPDATE_SCOPE_LOCAL)
.createToken()
.getTokenValue();
}
Note: The class
JwtGenerator
has the responsibility to generate a JWT Token for those scopes which are passed to thewithLocalScopes()
method. It returns the token in a format that is suitable for the HTTPAuthorization
header. The generator signs the JWT Token with the private key that was set on theSecurityTestRule
.
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)
The security library creates a configuration object that fits the environment it is run in by parsing the VCAP_SERVICES
environment variable. For the test we want to
override that parsing with test settings. This can be done by adding the @TestPropertySource
annotation right before the test class declaration.
Here the required properties can be overridden so that they match the information contained in the jwt
token that is generated for the test.
To simplify the setup we use the defaults from the SecurityTestRule
@TestPropertySource(properties = {
"xsuaa.uaadomain=" + SecurityTestRule.DEFAULT_DOMAIN,
"xsuaa.xsappname=" + SecurityTestRule.DEFAULT_APP_ID,
"xsuaa.clientid=" + SecurityTestRule.DEFAULT_CLIENT_ID })
//@formatter:off
public class AdvertisementControllerTest {
...
Now you can run the JUnit tests as described in Exercise 4. They should succeed now.
In this step you prepare the local run environment and test your application manually using Postman
to discover that your application is now secure.
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"}],"xsuaa":[{"credentials":{"clientid":"sb-clientId!t0815","clientsecret":"dummy-clientsecret","identityzone":"<<your tenant>>","identityzoneid":"a09a3440-1da8-4082-a89c-3cce186a9b6c","tenantid":"a09a3440-1da8-4082-a89c-3cce186a9b6c","uaadomain":"localhost","tenantmode":"shared","url":"dummy-url","verificationkey":"-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm1QaZzMjtEfHdimrHP3/2Yr+1z685eiOUlwybRVG9i8wsgOUh+PUGuQL8hgulLZWXU5MbwBLTECAEMQbcRTNVTolkq4i67EP6JesHJIFADbK1Ni0KuMcPuiyOLvDKiDEMnYG1XP3X3WCNfsCVT9YoU+lWIrZr/ZsIvQri8jczr4RkynbTBsPaAOygPUlipqDrpadMO1momNCbea/o6GPn38LxEw609ItfgDGhL6f/yVid5pFzZQWb+9l6mCuJww0hnhO6gt6Rv98OWDty9G0frWAPyEfuIW9B+mR/2vGhyU9IbbWpvFXiy9RVbbsM538TCjd5JF2dJvxy24addC4oQIDAQAB-----END PUBLIC KEY-----","xsappname":"xsapp!t0815"},"label":"xsuaa","name":"uaa-bulletinboard","plan":"application","tags":["xsuaa"]}]}
- If you run the application from the command line, update your
localEnvironmentSetup
script accordingly tolocalEnvironmentSetup.sh
(localEnvironmentSetup.bat
)
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.
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 the
setUp
method of theAdvertisementControllerTest
and run theJUnit
tests again to fetch the value ofjwt
from there.
Explanation: The generated JWT Token is an "individual one" as it
- contains specific scope(s) e.g.
xsapp!t0815.Display
. Furthermore note that the scope is composed of xsappname e.g.xsapp!t0815
which also needs to be the same as provided as part of theVCAP_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
Now you can test the service manually in the browser using the Postman
chrome plugin.
- You should get for any endpoint (except for
\health
) an401
("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 theVCAP_SERVICES
environment variable is provided on Tomcat as described above, another restart might be required.
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.
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 thebulletinboard-ads
application with the name of your XSUAA service:
- name: bulletinboard-ads
services:
...
- uaa-bulletinboard
- Now re-deploy your application to Cloud Foundry.
- Call your service endpoints e.g.
https://bulletinboard-ads-<<your user id>>.cfapps.<<region>>.hana.ondemand.com
manually using thePostman
Chrome plugin. You should get for any endpoint (except for\health
) an401
("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 theapprouter
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
withinPostman
. You might need to install anotherPostman Interceptor
Chrome Plugin, which will help you to send requests using browser cookies through thePostman
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.
- enable the
Note:
By default the application router enables CSRF protection for any state-changing HTTP method. That means that you need to provide ax-csrf-token: <token>
header for state-changing requests. You can obtain the<token>
via aGET
request with ax-csrf-token: fetch
header to the application router.
- Spring Security Reference
- Expression-Based Access Control
- Java Security library
- Java Security Test library
- Migration Guide for Applications that use Spring Security and java-container-security
-
© 2018 SAP SE