Security
Describes authentication and authorization in CAP Java.
Overview
With respect to web services, authentication is the act of proving the validity of user claims passed with the request. This typically comprises verifying the user's identity, tenant, and additional claims like granted roles. Briefly, authentication controls who is using the service. In contrast, authorization makes sure that the user has the required privileges to access the requested resources. Hence, authorization is about controlling which resources the user is allowed to handle.
Hence both, authentication and authorization, are essential for application security:
- Authentication describes how to configure authentication.
- Authorization describes how to configure access control.
WARNING
Without security configured, CDS services are exposed to public. Proper configuration of authentication and authorization is required to secure your CAP application.
Authentication
User requests with invalid authentication need to be rejected as soon as possible, to limit the resource impact to a minimum. Ideally, authentication is one of the first steps when processing a request. This is one reason why it's not an integral part of the CAP runtime and needs to be configured on application framework level. In addition, CAP Java is based on a modular architecture and allows flexible configuration of the authentication method. For productive scenarios, XSUAA and IAS authentication is supported out of the box, but a custom authentication can be configured as well. For the local development and test scenario, there's a built-in mock user support.
Configure XSUAA and IAS Authentication
To enable your application for XSUAA or IAS-authentication we recommend to use the cds-starter-cloudfoundry
or the cds-starter-k8s
starter bundle, which covers all required dependencies.
Individual Dependencies
These are the individual dependencies that can be explicitly added in the pom.xml
file of your service:
com.sap.cloud.security:resourceserver-security-spring-boot-starter
that brings spring-security libraryorg.springframework.boot:spring-boot-starter-security
cds-feature-identity
In addition, your application needs to be bound to corresponding service instances depending on your scenario. The following list describes which service needs to be bound depending on the tokens your applications should accept:
- only accept tokens issued by XSUAA --> bind your application to an XSUAA service instance
- only accept tokens issued by IAS --> bind your application to an IAS service instance
- accept tokens issued by XSUAA and IAS --> bind your application to service instances of both types.
Specify Binding
CAP Java picks only a single binding of each type. If you have multiple XSUAA or IAS bindings, choose a specific binding with property cds.security.xsuaa.binding
respectively cds.security.identity.binding
. Choose an appropriate XSUAA service plan to fit the requirements. For instance, if your service should be exposed as technical reuse service, make use of plan broker
.
Proof-Of-Possession for IAS
Proof-Of-Possession is a technique for additional security where a JWT token is bound to a particular OAuth client for which the token was issued. On BTP, Proof-Of-Possession is supported by IAS and can be used by a CAP Java application.
Typically, a caller of a CAP application provides a JWT token issued by IAS to authenticate a request. With Proof-Of-Possession in place, a mutual TLS (mTLS) tunnel is established between the caller and your CAP application in addition to the JWT token.
Clients calling your CAP application need to send the certificate provided by their identity
service instance in addition to the IAS token. On Cloud Foundry, the CAP application needs to be exposed under an additional route which accepts client certificates and forwards them to the application as X-Forwarded-Client-Cert
header (for example, the .cert.cfapps.<landscape>
domain).
The Proof-Of-Possession also affects approuter calls to a CAP Java application. The approuter needs to be configured to forward the certificate to the CAP application. This can be achieved by setting forwardAuthCertificates: true
on the destination pointing to your CAP backend (for more details see the environment destinations
section on npmjs.org).
When authenticating incoming requests with IAS, the Proof-Of-Possession is activated by default. This requires using at least version 3.5.1
of the SAP BTP Spring Security Client library.
You can disable the Proof-Of-Possession enforcement in your CAP Java application by setting the property sap.spring.security.identity.prooftoken
to false
in the application.yaml
file.
Automatic Spring Boot Security Configuration
Only if both, the library dependencies and an XSUAA/IAS service binding are in place, the CAP Java SDK activates a Spring security configuration, which enforces authentication for all endpoints automatically:
- Protocol adapter endpoints (managed by CAP such as OData V4/V2 or custom protocol adapters)
- Remaining custom endpoints (not managed by CAP such as custom REST controllers or Spring Actuators)
The security auto configuration authenticates all endpoints by default, unless corresponding CDS model is not explicitly opened to public with pseudo-role any
(configurable behaviour). Here's an example of a CDS model and the corresponding authentication configuration:
service BooksService @(requires: 'any') {
@readonly
entity Books @(requires: 'any') {...}
entity Reviews {...}
entity Orders @(requires: 'Customer') {...}
}
Path | Authenticated ? |
---|---|
/BooksService | ✗ |
/BooksService/$metadata | ✗ |
/BooksService/Books | ✗ |
/BooksService/Reviews | ✓1 |
/BooksService/Orders | ✓ |
1 Since version 1.25.0
TIP
For multitenant applications, it's required to authenticate all endpoints as the tenant information is essential for processing the request.
There are several application parameters in section cds.security.authentication
that influence the behaviour of the auto-configuration:
Configuration Property | Description | Default |
---|---|---|
mode | Determines the authentication mode: never , model-relaxed , model-strict or always | model-strict |
authenticateUnknownEndpoints | Determines, if security configurations enforce authentication for endpoints not managed by protocol-adapters. | true |
authenticateMetadataEndpoints | Determines, if OData $metadata endpoints enforce authentication. | true |
The following properties can be used to switch off automatic security configuration at all:
Configuration Property | Description | Default |
---|---|---|
cds.security.xsuaa.enabled | Whether automatic XSUAA security configuration is enabled. | true |
cds.security.identity.enabled | Whether automatic IAS security configuration is enabled. | true |
Setting the Authentication Mode
The property cds.security.authentication.mode
controls the strategy used for authentication of protocol-adapter endpoints. There are four possible values:
never
: No endpoint requires authentication. All protocol-adapter endpoints are considered public.model-relaxed
: Authentication is derived from the authorization annotations@requires
and@restrict
. If no such annotation is available, the endpoint is considered public.model-strict
: Authentication is derived from the authorization annotations@requires
and@restrict
. If no such annotation is available, the endpoint is authenticated. An explicit@requires: 'any'
makes the endpoint public.always
: All endpoints require authentication.
By default the authentication mode is set to model-strict
to comply with secure-by-default. In that case you can use the annotation @requires: 'any'
on service-level to make the service and its entities public again. Please note that it's only possible to make an endpoint public, if the full endpoint path is considered public as well. For example you can only make an entity public, if the service that contains it is also considered public.
TIP
Please note that the authentication mode has no impact on the authorization behaviour.
Customizing Spring Boot Security Configuration
If you want to explicitly change the automatic security configuration, you can add an additional Spring security configuration on top that overrides the default configuration by CAP. This can be useful, for instance, if an alternative authentication method is required for specific endpoints of your application.
@Configuration
@EnableWebSecurity
@Order(1) // needs to have higher priority than CAP security config
public class AppSecurityConfig {
@Bean
public SecurityFilterChain appFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher(AntPathRequestMatcher.antMatcher("/public/**"))
.csrf(c -> c.disable()) // don't insist on csrf tokens in put, post etc.
.authorizeHttpRequests(r -> r.anyRequest().permitAll())
.build();
}
}
Due to the custom configuration, all URLs matching /public/**
are opened for public access.
TIP
The Spring SecurityFilterChain
requires CAP Java SDK 1.27.x or later. Older versions need to use the deprecated WebSecurityConfigurerAdapter
.
❗ Warning
Be cautious with the configuration of the HttpSecurity
instance in your custom configuration. Make sure that only the intended endpoints are affected.
Another typical example is the configuration of Spring Actuators. For example a custom configuration can apply basic authentication to actuator endpoints /actuator/**
:
@Configuration
@EnableWebSecurity
@Order(1)
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain actuatorFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher(AntPathRequestMatcher.antMatcher("/actuator/**"))
.httpBasic(Customizer.withDefaults())
.authenticationProvider(/* configure basic authentication users here with PasswordEncoder etc. */)
.authorizeHttpRequests(r -> r.anyRequest().authenticated())
.build();
}
}
Custom Authentication
You're free to configure any authentication method according to your needs. CAP isn't bound to any specific authentication method or user representation such as introduced with XSUAA, it rather runs the requests based on a user abstraction. The CAP user of a request is represented by a UserInfo object that can be retrieved from the RequestContext as explained in Enforcement API & Custom Handlers.
Hence, if you bring your own authentication, you have to transform the authenticated user and inject as UserInfo
to the current request. This is done by means of UserInfoProvider interface that can be implemented as Spring bean as demonstrated in Registering Global Parameter Providers. More frequently you might have the requirement to just adapt the request's UserInfo
which is possible with the same interface:
@Component
public class CustomUserInfoProvider implements UserInfoProvider {
private UserInfoProvider defaultProvider;
@Override
public UserInfo get() {
ModifiableUserInfo userInfo = UserInfo.create();
if (defaultProvider != null) {
UserInfo prevUserInfo = defaultProvider.get();
if (prevUserInfo != null) {
userInfo = prevUserInfo.copy();
}
}
if (userInfo != null) {
XsuaaUserInfo xsuaaUserInfo = userInfo.as(XsuaaUserInfo.class);
userInfo.setName(xsuaaUserInfo.getEmail() + "/" +
xsuaaUserInfo.getOrigin()); // adapt name
}
return userInfo;
}
@Override
public void setPrevious(UserInfoProvider prev) {
this.defaultProvider = prev;
}
}
In the example, the CustomUserInfoProvider
defines an overlay on the default XSUAA-based provider (defaultProvider
). The overlay redefines the user's name by a combination of email and origin.
Mock User Authentication with Spring Boot
By default, CAP Java creates a security configuration, which accepts mock users for test purposes.
Preconfigured Mock Users
For convenience, the runtime creates default mock users reflecting the pseudo roles. They are named authenticated
, system
and privileged
and can be used with an empty password. For instance, requests sent during a Spring MVC unit test with annotation @WithMockUser("authenticated")
will pass authorization checks that require authenticated-user
. The privileged user will pass any authorization checks. cds.security.mock.defaultUsers = false
prevents the creation of default mock users at startup.
Explicitly Defined Mock Users
You can also define mock users explicitly. This mock user configuration only applies if:
- The service runs without an XSUAA service binding (non-productive mode)
- Mock users are defined in the active application configuration
Define the mock users in a Spring profile, which may be only active during testing, as in the following example:
---
spring:
config.activate.on-profile: test
cds:
security:
mock:
users:
- name: Viewer-User
password: viewer-pass
tenant: CrazyCars
roles:
- Viewer
attributes:
Country: [GER, FR]
additional:
email: myviewer@crazycars.com
features:
- cruise
- park
- name: Privileged-User
password: privileged-pass
privileged: true
features:
- "*"
- Mock user with name
Viewer-User
is a typical business user with SaaS-tenantCrazyCars
who has assigned roleViewer
and user attributeCountry
($user.Country
evaluates to value list[GER, FR]
). This user also has the additional attributeemail
, which can be retrieved withUserInfo.getAdditionalAttribute("email")
. The featurescruise
andpark
are enabled for this mock user. Privileged-User
is a user running in privileged mode. Such a user is helpful in tests that bypasses all authorization handlers.
Property cds.security.mock.enabled = false
disables any mock user configuration.
A setup for Spring MVC-based tests based on the given mock users and the CDS model from above could look like this:
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class BookServiceOrdersTest {
String ORDERS_URL = "/odata/v4/BooksService/Orders";
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(username = "Viewer-User")
public void testViewer() throws Exception {
mockMvc.perform(get(ORDERS_URL)).andExpect(status().isOk());
}
@Test
public void testUnauthorized() throws Exception {
mockMvc.perform(get(ORDERS_URL)).andExpect(status().isUnauthorized());
}
}
Mock Tenants
A tenants
section allows to specify additional configuration for the mock tenants. In particular it is possible to assign features to tenants:
---
spring:
config.activate.on-profile: test
cds:
security:
mock:
users:
- name: Alice
tenant: CrazyCars
tenants:
- name: CrazyCars
features:
- cruise
- park
The mock user Alice
is assigned to the mock tenant CrazyCars
for which the features cruise
and park
are enabled.
Authorization
CAP Java SDK provides a comprehensive authorization service. By defining authorization rules declaratively via annotations in your CDS model, the runtime enforces authorization of the requests in a generic manner. Two different levels of authorization can be distinguished:
- Role-based authorization allows to restrict resource access depending on user roles.
- Instance-based authorization allows to define user privileges even on entity instance level, that is, a user can be restricted to instances that fulfill a certain condition.
It's recommended to configure authorization declaratively in the CDS model. If necessary, custom implementations can be built on the Authorization API.
A precise description of the general authorization capabilities in CAP can be found in the Authorization guide.
Role-Based Authorization
Use CDS annotation @requires
to specify in the CDS model which role a user requires to access the annotated CDS resources such as services, entities, actions, and functions (see Restricting Roles with @requires). The generic authorization handler of the runtime rejects all requests with response code 403 that don't match the accepted roles. More specific access control is provided by the @restrict
annotation, which allows to combine roles with the allowed set of events. For instance, this helps to distinguish between users that may only read an entity from those who are allowed to edit. See section Control Access with @restrict to find details about the possibilities.
Instance-Based Authorization
Whereas role-based authorization applies to whole entities only, Instance-Based Authorization allows to add more specific conditions that apply on entity instance level and depend on the attributes that are assigned to the request user. A typical use case is to narrow down the set of visible entity instances depending on user properties (for example, CountryCode
or Department
). Instance-based authorization is also basis for domain-driven authorizations built on more complex model constraints.
Current Limitations
The CAP Java SDK translates the where
-condition in the @restrict
annotation to a predicate, which is appended to the CQN
statement of the request. This applies only to READ
,UPDATE
, and DELETE
events. In the current version, the following limitations apply:
- For
UPDATE
andDELETE
events no paths in thewhere
-condition are supported. - Paths in
where
-conditions withto-many
associations or compositions can only be used with anexists
predicate. UPDATE
andDELETE
requests that address instances that aren't covered by the condition (for example, which aren't visible) aren't rejected, but work on the limited set of instances as expected. As a workaround for the limitations with paths inwhere
-conditions, you may consider using theexists
predicate instead.
CAP Java SDK supports User Attribute Values that can be referred by $user.<attribute-name>
in the where-clause of the @restrict
-annotation. Currently, only comparison predicates with user attribute values are supported (<,<=,=,=>,>
). Note that generally a user attribute represents an array of strings and not a single value. A given value list [code1, code2]
for $user.code
in predicate $user.code = Code
evaluates to (code1 = Code) or (code2 = Code)
in the resulting statement.
Enforcement API & Custom Handlers
The generic authorization handler performs authorization checks driven by the annotations in an early Before handler registered to all application services by default. You may override or add to the generic authorization logic by providing custom handlers. The most important piece of information is the UserInfo that reflects the authenticated user of the current request. You can retrieve it:
a) from the EventContext:
EventContext context;
UserInfo user = context.getUserInfo();
b) through dependency injection within a handler bean:
@Autowired
UserInfo user;
The most helpful getters in UserInfo
are listed in the following table:
UserInfo method | Description |
---|---|
getName() | Returns the unique (logon) name of the user as configured in the IdP. Referred by $user and $user.name . |
getTenant() | Returns the tenant of the user. |
isSystemUser() | Indicates whether the request has been initiated by a technical service. Refers to pseudo-role system-user . |
isAuthenticated() | True if the current user has been authenticated. Refers to pseudo-role authenticated-user . |
isPrivileged() | Returns true if the current user runs in privileged (that is, unrestricted) mode |
hasRole(String role) | Checks if the current user has the given role. |
getRoles() | Returns the roles of the current user |
getAttributeValues(String attribute) | Returns the value list of the given user attribute. Referred by $user.<attribute> . |
It's also possible to modify the UserInfo
object for internal calls. See section Request Contexts for more details. For instance, you might want to run internal service calls in privileged mode that bypasses authorization checks:
cdsRuntime.requestContext().privilegedUser().run(privilegedContext -> {
assert privilegedContext.getUserInfo().isPrivileged();
// ... Service calls in this scope pass generic authorization handler
});