The following steps will explain how to secure applications in SAP Business Technology Platform, Cloud Foundry (SAP BTP CF) which are built based on the Spring Boot.
This Spring Boot 2.0 demo application shows how to implement basic access control in Spring based SAP Business Technology Platform applications. It leverages Spring Security 5.x and integrates to SAP Business Technology Platform XSUAA service (OAuth Resource Server) using the SAP Container Security Library (Java), which is available on maven central.
In order to limit access to certain instances, you can restrict the access to specific function by Roles (Scopes). Or, even more fine granular, you can restrict the access on data level so that different users can see and maintain different subsets of the data instances depending on certain user dependent attribute values.
The microservice is a Spring boot version of the code developed in the openSAP course: Cloud-Native Development with SAP CP and runs in the Cloud Foundry environment within SAP Business Technology Platform.
Note: The new
SAP Java Client Security Library
validates the access token, which is in JSON Web Token format, locally (offline). For verifying the signature of the access token it periodically retrieves and caches the JSON Web Keys (JWK) from the Authorization Server. As consequence, in order to test our Spring Boot application locally, or as part of our JUnit tests, we have to provide a Mock Web Server that mocks the/token_keys
endpoint that returns JWKs. Thus, this sample starts and configures a Mock Web Server for the OAuth 2.0 Authorization Server as explained here. The mock server is only started in case theuaamock
Spring profile is active.
This document is divided into the following sections
- Understanding OAuth 2.0 Components - introduces the SAP components that make up the OAuth setup
- Use Cases - Role-Based Access Control (RBAC) and Attribute-Based Access Control (ABAC)
- Download and Installation - a description of how to use this project
- Steps to Deploy and Test on Cloud Foundry - explains how to deploy and test the application on Cloud Foundry
- Further Refererences - references and further learning material
To better understand the content of this sample, you should have a rough understanding about the SAP BTP OAuth 2.0 components that are introduced here.
In general we want to distinguish here between Role-Based Access Control (RBAC) and Attribute-Based Access Control (ABAC).
RBAC controls access based on the roles that users have within the system whereas ABAC is more flexible as you can use attributes (user specific, resource / application specific attributes or environmental conditions) to implement instance-based access control. Any attribute can be used as "filter" to allow a more fine-grain access control to a resource.
Your application wants to limit the access to particular application functions, but there are no further restrictions on data level required. In this case the application developer needs to introduce a set of Roles
as part of the security model and attach these Role
-checks to the functions that should be secured. With that only users who have the required roles assigned are permitted to perform the restricted functions.
In our example:
- User with role "Viewer" (scope: "Display") can read all public advertisements (in his tenant).
- User with role "Advertiser" (scopes: "Update", "Display") can additionally create / update and delete public advertisements (in his tenant).
Scopes, Role-templates and Role-collections are specified by the application developer as part of the application security model (xs-security.json
).
Options to implement basic role-based authorization checks:
- Configure Application Router to restrict access to HTTP endpoints in order to avoid unnecessary traffic (
xs-app.json
). - Course-grained role checks for HTTP endpoints on application level to secure direct requests to application (Spring Security Configuration).
- Fine-grained role checks on method level using method access-control expressions like
@PreAuthorize("hasAuthority('Display')")
(Spring Web Controller). Here you need to enable Method Security (Method Security Configuration).
In addition to the functional separation you want to restrict the modification and deletion of a resource to the resource owner, the one who has created the advertisement.
When Method Security is enabled (Method Security Configuration) this can be easily implemented by referring to a bean as part of the Web Security Expression that implements the isCreatedBy
check @PreAuthorize("@webSecurity.isCreatedBy(#id)")
(Web Security Expressions).
There is also the option to use hasPermission()
expressions. Then you have to explicitly configure a PermissionEvaluator in your application context. This and much more about built-in method security expressions are documented on Spring.io documentation.
In addition to the functional separation you want to limit the access on data level so that different users can see and maintain different subsets of the data instances depending on certain user dependent attribute values. These attribute values can be derived from the users main data maintained in the application like cost center, organization code, department, country, location, etc. or can be set by an authorization administrator and will then be part of the user role assignment. During data access by the user this authorization method automatically applies a filter in general by enhancing the WHERE condition of the SQL operation where the user attribute is compared to an attribute of the accessed data object.
In our example it should be possible to restrict access to instances conditional on "confidentiality_level" classification, e.g. "Public", "Internal", "Confidential" or "Strictly confidential".
- Users with role collection "RC_ViewerPUBLIC" (scope: "Display") can only read their own advertisements and advertisements with
confidentiality_level=Public
. - Users with role collection "RC_AdvertiserPUBLIC" (scopes: "Update", "Display") can additionally create any advertisement and update all advertisements.
- Users with role collection "RC_AdvertiserALL" (scopes: "Update", "Display") and attribute
confidentiality_level=Strictly confidential
(highest classification) can furthermore read any advertisement.
Attributes and Role-templates are specified by the application developer as part of the application security model (xs-security.json
).
- The application needs to support values-request for the application-specific attributes, e.g.
confidentiality_level
(Example). - The application needs to restrict any kind of read access (single read, read all,...) to non-public advertisements to users that are not owner and do not have access granted based on required attribute (Example).
Setup your development environment according to the description here. There is no need to install Docker.
To run the application locally, you have two options: start it directly via Maven on the command line or within your IDE (Eclipse, IntelliJ).
In both cases, your application will be deployed to an embedded Tomcat web server and will be accessible at the address http://localhost:8080/api/v1/ads
. The endpoints of the application are secured, i.e. the endpoints will response with status code 401
(Unauthenticated) unless you provide a valid JWT token.
Note: The application needs to interact with the XSUAA service to validate the JWT tokens. To avoid this interaction during test execution and as well during local testing we connect to a local XSUAA Mock Web Server instead. This Mock Web Server serves some XSUAA service endpoints such as
/token_keys
. With activation ofuaamock
profile, the Mock Web Server gets started and configured according to the settings specified in the application-uaamock.properties file.
You can find here a more detailed description on how to setup the XSUAA Mock Web Server in your Spring boot application.
The provided localEnvironmentSetup
shell script can be used to set the necessary values for local execution. Within your development IDE (Eclipse, IntelliJ), you need to define the VCAP_APPLICATION
and the SPRING_PROFILES_ACTIVE
environment variables - as done in the script.
Execute in terminal (within project root, which contains the pom.xml
):
source localEnvironmentSetup.sh
mvn spring-boot:run
Or on Windows command line:
localEnvironmentSetup.bat
mvn spring-boot:run
In Eclipse Spring Tool Suite (STS), you can import the project as an existing Maven project. There you can start the main method in com.sap.cp.appsec.Application
.
You can also right-click on the class in the Package Explorer, and select Run As
- Spring Boot App
.
Make sure that you have set the same environment variables in the Run Configuration as specified in the
localEnvironmentSetup
script.
The application endpoints are secured, that means you should get for any endpoint (except for /actuator/health
) an 401 ("unauthorized") status code. The application expects a digitally signed JWT as part of the Authorization header to simulate that you are an authenticated user with the scopes/roles required to access the protected endpoints.
Have a look into the AdvertisementControllerTest
test class. There we make use of the JwtGenerator
from the spring-xsuaa-test
library for generating some JWT tokens with different scopes and attribute values in order to test whether the application behaves correctly and the endpoints are properly protected:
- missing JWT should result in
not authenticated (401)
- expired or invalid signed JWT should result in
not authenticated (401)
- JWT with missing scopes should result in
forbidden / not authorized (403)
Explanation: The generated JWT Token is an "individual one" as it
- contains specific scope(s) e.g.
<<your appId>>.Display
(<<your appId>>
matches thexsuaa.xsappname
property or as part ofVCAP_SERVICES
--xsuaa
--xsappname
system environment variable).- it is signed with a private key that fits to the public key (JWK) of the XSUAA (or Mock Web Server).
For a better understanding you can set a breakpoint in the setup
method of the AdvertismentControllerTest
test, start the test in debugging mode, fetch a JWT and decode it on this web site: https://jwt.io/.
Now you are ready to test the application manually using the Postman
chrome plugin.
Test the REST endpoint http://localhost:8080/api/v1/ads
manually using the Postman
chrome extension.
You can import the Postman collection, as well as the Postman environment that provides different JWT tokens for the Authorization
headers to do some sample requests.
Note: For all requests make sure, that you provide a header namely Authorization
with a JWT token as value e.g. Bearer eyJhbGciOiJSUzI1NiIs...
.
For reference look up Postman documentation.
Build the Advertisement Service which is a Java web application running in a Java VM. Maven build tool compiles the code and packages it in its distributable format, such as a JAR
(Java Archive). With this the maven dependencies are downloaded from the Maven central into the ~/.m2/repository
directory. Furthermore the JUnit tests are executed and the target/demo-application-security-basis-1.0.jar
is created.
Execute in the command line (within project directory, which contains the
pom.xml
):
mvn package
Make sure your are logged in to Cloud Foundry and you target your trial space. The following commands will setup your environment to use the provided Cloud Foundry instance.
cf api <<Your API endpoint>>
(API endpoints are listed here)cf login -u <<your user id>>
- In case you are assigned to multiple orgs, select the
trial
organisation.
Create the (backing) service that is specified in the manifest.yml
.
Execute in terminal (within project directory, which contains the security
folder):
cf create-service xsuaa application uaa-bulletinboard -c security/xs-security.json
Using the marketplace (
cf m
) you can see the backing services and its plans that are available on SAP BTP and (!) you are entitled to use.
As a prerequisite step open the ../vars.yml file locally and replace the ID
for example by your SAP account user name, e.g. p0123456
, to make the routes unique. You might want to adapt the LANDSCAPE_APPS_DOMAIN
as well.
The application can be built and pushed using these commands (within root directory, which contains the manifest.yml
):
cf push --vars-file ../vars.yml
The application will be pushed using the settings provided in the
manifest.yml
and../vars.yml
. You can get the exact urls/routes of your deployed application withcf apps
.
We make use of the trial
subaccount. As you can see in the SAP BTP Cockpit subaccounts have properties (see Subaccount Details) which of the most important one is the Subdomain. The Subdomain serves as the value for the technical property Tenant ID, e.g. p0123456trial
.
The Tenant ID is encoded in the url, for example https://<<your tenant ID>>-approuter-<<ID>>.<<LANDSCAPE_APPS_DOMAIN>>
.
That's why we need to specify another route for the approuter application for each Tenant ID (subdomain name). For example:
cf map-route approuter <<LANDSCAPE_APPS_DOMAIN e.g. cfapps.eu10.hana.ondemand.com>> -n <<your tenant ID e.g. p0123456trial>>-approuter-<<ID e.g. p0123456>>
After that you should see routes similar to these when calling cf routes
:
space host domain apps
dev bulletinboard-ads-p0123456 cfapps.eu10.hana.ondemand.com bulletinboard-ads
dev approuter-p0123456 cfapps.eu10.hana.ondemand.com approuter
dev p0123456trial-approuter-p0123456 cfapps.eu10.hana.ondemand.com approuter
And cf app approuter
shows another tenant-specific approuter route, which is hereinafter also called "approuterUri
".
-
Call your application endpoints e.g.
https://bulletinboard-ads-<<ID>>.cfapps.<<region>>.hana.ondemand.com
. You should get for any endpoint (except for/actuator/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 the tenant-specific approuter URI in order to authenticate yourself and to create a JWT with no scopes. So, open Chrome browser and call the tenant-specificapprouterUri
URL as created here. This will bring you the login page. You have to enter here your SAP Cloud Identity credentials. After successful login you get redirected to the advertisement application that returns you the health status.Note: This
xs-app.json
file specifies how the approuter routes are mapped to the advertisement routes. E.g.<<approuterUri>>/ads/actuator/health
maps to<<bulletinboardAdsUri>>/actuator/health
. -
Now test the deployed Web application on Cloud Foundry via the approuter url using the
Postman
chrome plugin together with thePostman Interceptor
chrome plugin. You can import the Postman collection and create within Postman an environment, which specifies the key-value pairapprouterUri
=<<your tenant-specific approuterUri>>
. When calling the tenant-specific approuter URI as part ofPostman
chrome plugin you probably will get as response the login screen in HTML. That's why you need to- enable the
Interceptor
withinPostman
AND within Chrome browser. 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.{{approuterUri}}/ads/api/v1/ads/
and - make sure that you now get a
403
status code.
- enable the
Finally, as part of your Identity Provider, e.g. SAP ID Service, assign the deployed Role Collection(s) to your user as depicted in the screenshot below and as documented here.
Further up-to-date information you can get on sap.help.com:
According to the Role Collection(s) you've assigned to your user you should have read/write access to your application endpoints.
You need to logon again to your application so that the authorities are assigned to your user's JWT. You can provoke a logon screen when clearing your cache.
Call again your application endpoints via the approuter Uri using the Postman
Chrome plugin as explained here. You should now be authorized to create, read and delete advertisements.
Troubleshoot You can analyze the authorities that are assigned to the current user via
https://<<your tenant>>.authentication.<<LANDSCAPE_APPS_DOMAIN>>/config?action=who
CSRF protection 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 aGET
request with ax-csrf-token: fetch
header to the application router. In this sample the csrf protection is disabled as part of the application router configuration (xs-app.json
).