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
.
-
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
)!
- Create a
WebSecurityConfig
class in the packagecom.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 ofprivate static final String XSAPPNAME
must be equal to the value ofxsappname
, that is defined in yourxs-security.json
file.
Technically - under the hood - the default
AffirmativeBased
AccessDecisionManager
is used. This holds theWebExpressionVoter
, which in turn makes use of theOAuth2WebSecurityExpressionHandler
that handles Spring EL expressions likehasRole
orisAuthenticated
(read more). If you require more than one Voter you can specify a "custom"AccessDecisionManager
such asUnanimousBased
.
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.
-
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 namedcom.sap.bulletinboard.ads.config
.
In productive environments,SAPOfflineTokenServicesCloud
reads the public key value from the environment variableVCAP_SERVICES
. For unit tests, you explicitly set the public key of your test key pair with theJwtGenerator
. TheJwtGenerator
takes the public key from thepublicKey.txt
file. -
Copy the implementation of the
JwtGenerator
class from here into a new test package namedcom.sap.bulletinboard.ads.testutils
.
- 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 thegetTokenForAuthorizationHeader()
method as a String array. It returns the token in a format that is suitable for the HTTPAuthorization
header. The generator signs the JWT Token with its private key (taken from fileprivateKey.txt
).
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)
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"}],"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 tolocalEnvironmentSetup.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.
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 packagecom.sap.bulletinboard.ads.testutils
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.
bulletinboard-<<your user id>>.Display
(as defined in yourWebSecurityConfig
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 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.
-
© 2018 SAP SE