This project provides validation of tokens issued by SAP BTP Identity service or XSUAA for Spring Boot applications.
It fully integrates java-security
with Spring Security OAuth 2.0 Resource Server by providing the following key features:
- Automatic OAuth2 service configuration based on SAP BTP service bindings found in the environment
- OAuth2 Token Validation based on these service configurations
- Easy access to principal and token claims within request handlers
- Fetch XSUAA tokens with different Grant types
- Cloud Foundry
- Kubernetes/Kyma
- SAP Identity service, supports Multitenancy/multiple zones
- XSUAA
JWS | Algorithm | Description |
---|---|---|
RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 |
- Java 17
- Spring Boot 3
- Spring Framework 6
- Apache HttpClient 4.5
These (spring) dependencies need to be provided:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.sap.cloud.security</groupId>
<artifactId>resourceserver-security-spring-boot-starter</artifactId>
<version>3.5.4</version>
</dependency>
By using resourceserver-security-spring-boot-starter
, beans that are required to initialize the Spring Boot application as OAuth resource server are autoconfigured.
The integration into Spring Security is done by providing a bean of type JwtDecoder that overrides the default of the Spring framework.
Depending on the service bindings in the environment, a different implementation is used to support both SAP Identity Service and XSUAA.
In addition, a bean of type XsuaaTokenFlows is provided that can be used to fetch XSUAA tokens.
Autoconfiguration class | Description |
---|---|
HybridAuthorizationAutoConfiguration | Creates a converter (XsuaaTokenAuthorizationConverter) that removes the XSUAA application identifier from the scope names, allowing local scope checks to be performed using Spring's common built-in expression hasAuthority . Supports only single Xsuaa binding |
HybridIdentityServicesAutoConfiguration | Configures a JwtDecoder which is able to decode and validate tokens from Xsuaa and/or Identity serviceFurthermore it registers IdentityServiceConfiguration and optionally XsuaaServiceConfiguration , that allow overriding the identity service configurations found in the service bindings (via identity.* and xsuaa.* properties). |
XsuaaTokenFlowAutoConfiguration | Configures an XsuaaTokenFlows bean to fetch the XSUAA tokens. Starting with 2.10.0 version it supports X.509 based authentication |
SecurityContextEnvironmentPostProcessor | Configures JavaSecurityContextHolderStrategy to be used as SecurityContextHolderStrategy to keep the com.sap.cloud.security.token.SecurityContext in sync |
Autoconfiguration property | Default value | Description |
---|---|---|
sap.spring.security.hybrid.auto | true | This enables all auto-configurations that setup your project for hybrid IAS and/or XSUAA token validation. |
sap.spring.security.xsuaa.flows.auto | true | This enables all auto-configurations required for XSUAA token exchange using token-client library. |
sap.spring.security.identity.prooftoken | true | This creates a JwtDecoder for identity service with enabled prooftoken check |
You can gradually replace auto-configurations as explained here.
For example, to create a converter that removes the application identifier of the first XSUAA configuration from the scope names, you could create the following bean:
@Bean
public Converter<Jwt, AbstractAuthenticationToken> xsuaaAuthConverter(XsuaaServiceConfigurations xsuaaConfigs) {
return new XsuaaTokenAuthorizationConverter(xsuaaConfigs.getConfigurations().get(0).getProperty(APP_ID));
}
A further example can be found here.
You may want to filter the list accessible via XsuaaServiceConfigurations#getConfigurations
based on the configuration
properties to find a specific configuration from the list.
This is an example how to configure your application as Spring Security OAuth 2.0 Resource Server for authentication of HTTP requests:
@Configuration
@EnableWebSecurity
@PropertySource(factory = IdentityServicesPropertySourceFactory.class, ignoreResourceNotFound = true, value = { "" })
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
Converter<Jwt, AbstractAuthenticationToken> authConverter; // only required for XSUAA
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// ... secure endpoints based on Authorities ...
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(authConverter); // (1) you may want to provide your own converter
}
}
💡 Please note that the autoconfigured authentication converter only supports
hasAuthority
-checks for scopes provided with the Xsuaa access token. In case you need to consider authorizations provided via an OIDC token from IAS you need to provide your own converter instead.
You may want to configure the security chain with your own Authorization Converter by implementing the Converter<Jwt, AbstractAuthenticationToken>
interface.
Here is an example implementation that provides authorities based on Identity service groups.
The leading prefix "IASAUTHZ_" is removed for easier authorization checks.
The implementation delegates to the default authConverter
in case of an Xsuaa access token.
In this sample, it is expected to be autowired in the configuration class in which you define your converter.
class MyCustomTokenAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> {
public AbstractAuthenticationToken convert(Jwt jwt) {
if(jwt.containsClaim(TokenClaims.XSUAA.EXTERNAL_ATTRIBUTE)) { // required in case of XSUAA
return authConverter.convert(jwt); // @Autowired Converter<Jwt, AbstractAuthenticationToken> authConverter;
}
return new AuthenticationToken(jwt, deriveAuthoritiesFromGroup(jwt));
}
private Collection<GrantedAuthority> deriveAuthoritiesFromGroup(Jwt jwt) {
Collection<GrantedAuthority> groupAuthorities = new ArrayList<>();
if (jwt.containsClaim(TokenClaims.GROUPS)) {
List<String> groups = jwt.getClaimAsStringList(TokenClaims.GROUPS);
for (String group: groups) {
groupAuthorities.add(new SimpleGrantedAuthority(group.replace("IASAUTHZ_", "")));
}
}
return groupAuthorities;
}
}
Starting with version 2.13.0, the service bindings are read with btp-environment-variable-access.
Please adhere to the guidelines outlined here for configuring K8s secrets for the bound service configurations.
An example how to use spring-security
library in Kubernetes/Kyma environment can be found in spring-security-hybrid-usage.
Generally, the library supports out of the box services provisioned by SAP BTP service-operator. To access service instance configurations from the application, Kubernetes secrets need to be provided as files in a volume mounted on application's container.
- BTP Service-operator up to v0.2.2 - Library will look up the configuration files in the following paths:
- XSUAA:
/etc/secrets/sapbtp/xsuaa/<YOUR XSUAA INSTANCE NAME>
- Identity Services:
/etc/secrets/sapbtp/identity/<YOUR IDENTITY SERVICE INSTANCE NAME>
- XSUAA:
- BTP Service-operator starting from v0.2.3 - Library reads the configuration from k8s secret that is stored in a volume, this volume's
mountPath
must be defined in environment variableSERVICE_BINDING_ROOT
.- Upon creation of the service binding, a Kubernetes secret with the same name as the binding is created. This binding secret needs to be stored to pod's volume.
- The
SERVICE_BINDING_ROOT
environment variable needs to be defined with a value that points to volume mount's directory (mounthPath
) where the service binding secret will be stored. e.g. like here.
Controller endpoints can be secured based on the Authorities extracted by the Authorization Converter.\
❗ Never forget to finish your matcher chain with .anyRequest().denyAll()
For instance, to secure endpoint /helloWorld
for users with authority "Read".
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests(authz ->
authz
.requestMatchers("/helloWorld").hasAuthority("Read")
.anyRequest().denyAll())
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(<yourAuthorizationConverter>)
return http.build();
}
Spring Security supports authorization semantics at the method level. As prerequisite you need to enable global Method Security as explained in Baeldung tutorial: Introduction to Spring Method Security.
@GetMapping("/hello-token")
@PreAuthorize("hasAuthority('Read')")
public Map<String, String> message() {
...
}
You can use the @AuthenticationPrincipal
annotation to inject a Token object into your request handlers.
It provides different methods to access the token information, e.g. to extract user information:
@GetMapping("/getGivenName")
public String getGivenName(@AuthenticationPrincipal Token token) {
return token.getClaimAsString(TokenClaims.GIVEN_NAME)
}
💡 Make sure you've imported the right Token:
com.sap.cloud.security.token.Token
. There is more than one Token interface in this repository.
Please refer to the token-client documentation for information on how to use the provided XsuaaTokenFlows
bean to fetch XSUAA tokens.
In case you need information from the service binding configuration from one of the identity services, you have these options:
-
in case you are bound to a single
XSUAA
service instance:@Autowired XsuaaServiceConfiguration xsuaaServiceConfiguration;
-
in case you are bound to multiple
XSUAA
service instances@Autowired XsuaaServiceConfigurations xsuaaServiceConfigurations;
-
in case you are bound to an
identity
service instance@Autowired IdentityServiceConfiguration identityServiceConfiguration;
Alternatively, you can also access the information with Environments.getCurrent()
, which is provided with java-security
.
Show optional usage instructions
In case you have implemented a central Exception Handler as described with Baeldung Tutorial: Error Handling for REST with Spring you may want to emit logs to the audit log service in case of AccessDeniedException
s.
Alternatively there are also various options provided with Spring.io
. For example, you can integrate SAP audit log service with Spring Boot Actuator audit framework as described here.
In case of non-HTTP requests, you may need to initialize the Spring Security Context with a JWT token you've received from a message, an event or you've requested from the identity service directly:
import org.springframework.security.oauth2.jwt.JwtDecoder;
public class Listener {
@Autowired
JwtDecoder jwtDecoder;
@Autowired
Converter<Jwt, AbstractAuthenticationToken> authConverter;
public void onEvent(String encodedToken) {
if (encodedToken != null) {
SpringSecurityContext.init(encodedToken, jwtDecoder, authConverter);
}
try {
handleEvent();
} finally {
SpringSecurityContext.clear();
}
}
}
In detail com.sap.cloud.security.token.SpringSecurityContext
wraps the Spring Security Context (namely SecurityContextHolder.getContext()
), which stores by default the information in ThreadLocal
s. In order to avoid memory leaks it is recommended to remove the current thread's value for garbage collection.
💡 Note that
SpringSecurityContext
is thread-bound and is NOT propagated to child-threads. This Baeldung tutorial: Spring Security Context Propagation article provides more information on how to propagate the context.
In case you want to implement a reactive token authentication flow, you can use the ReactiveHybridJwtDecoder and the ReactiveSecurityContext. The reactive authentication flow allows to build non-blocking, asynchronous and event-driven applications.
We recommend java-security-test to write JUnit tests for the security layer of your application that runs without a real identity service instance.
It offers test utilities to generate custom JWT tokens for the purpose of tests.
It pre-configures a WireMock web server to stub outgoing calls to the identity service, e.g. to provide token keys for offline token validation.
If you need to manually configure the identity service configuration, e.g. to target the mocked OAuth2 server from java-security-test
, you can override the values read from the service bindings by setting the Spring properties sap.security.services.identity
or sap.security.services.xsuaa
.
In an application.yml
the test configuration suitable for use with java-security-test
would look as follows:
sap.security.services:
identity:
clientid: sb-clientId!t0815 # SecurityTest.DEFAULT_CLIENT_ID
domains:
- localhost # SecurityTest.DEFAULT_DOMAIN
xsuaa:
xsappname: xsapp!t0815 # SecurityTest.DEFAULT_APP_ID
uaadomain: localhost # SecurityTest.DEFAULT_DOMAIN
clientid: sb-clientId!t0815 # SecurityTest.DEFAULT_CLIENT_ID
url: http://localhost # SecurityTest.DEFAULT_URL
If you need to manually configure the application for more than one XSUAA service instances (e.g. one of
plan application
and another one of plan broker
).
sap.security.services:
xsuaa[0]:
... # credentials of XSUAA of plan 'application'
xsuaa[1]:
clientid: # clientid of XSUAA of plan 'broker'
To run or debug your secured application locally you need to provide the mandatory Xsuaa or Identity service configuration attributes prior to launching the application. There are two ways how to provide the service configuration to your Spring Boot application:
-
As Spring properties in
application.yaml
orapplication.properties
filesThe security library requires the following key value pairs to start successfully:
- For Xsuaa
sap.security.services: xsuaa: xsappname: xsapp!t0815 # SecurityTest.DEFAULT_APP_ID uaadomain: localhost # SecurityTest.DEFAULT_DOMAIN clientid: sb-clientId!t0815 # SecurityTest.DEFAULT_CLIENT_ID url: http://localhost # SecurityTest.DEFAULT_URL
- For Identity service
sap.security.services: identity: clientid: sb-clientId!t0815 # SecurityTest.DEFAULT_CLIENT_ID domains: - localhost # SecurityTest.DEFAULT_DOMAIN
💡 The provided values above correspond with the JwtGenerator default values from
java-security-test
library, meaning you can generate tokens and test them with this service configuration. - For Xsuaa
-
As
VCAP_SERVICES
environment variable The value of theVCAP_SERVICES
environment variable needs to be in the following format{"xsuaa": [ { "credentials": { "clientid": "sb-clientId!t0815", "xsappname": "xsapp!t0815", "uaadomain": "localhost", "url": "https://localhost" } } ] }
💡 To evaluate your application using an actual Identity service, you can obtain the service configuration information from the Identity or Xsuaa service instance created in the SAP BTP Cockpit. Then, use this data to populate the application.yml or the VCAP_SERVICES environment variable.
In case you face issues, submit an issue on GitHub and include the following details:
- any security-related dependencies used including version, get maven dependency tree with
mvn dependency:tree
- debug logs
- issue you’re facing.
First, configure the Debug log level for Spring Framework Web and all Security related libs. This can be done as part of your application.yml
or application.properties
file.
logging.level:
com.sap.cloud.security: DEBUG # set SAP-class loggers to DEBUG; set to ERROR for production setup
org.springframework: ERROR # set to DEBUG to see all beans loaded and autoconfig conditions met
org.springframework.security: DEBUG # set to ERROR for production setup
org.springframework.web: DEBUG # set to ERROR for production setup
Then, in case you want to view the various filters applied to a specific request, you can enable the debug flag by setting it to true in the @EnableWebSecurity
annotation:
@Configuration
@EnableWebSecurity(debug = true) // TODO "debug" may include sensitive information. Do not use in a production system!
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// ...
}
💡 Remember to restage your application for the changes to take effect.
We recognized that this error is raised, when your instance name contains upper cases.
When you're trying to run the application locally, but application fails to start with the following error message:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.core.convert.converter.Converter<org.springframework.security.oauth2.jwt.Jwt, org.springframework.security.authentication.AbstractAuthenticationToken>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
***************************
APPLICATION FAILED TO START
***************************
Field authConverter in com.sap.cloud.test.SecurityConfiguration required a bean of type 'org.springframework.core.convert.converter.Converter' that could not be found.
Make sure that you have defined the following mandatory attribute in the service configuration (VCAP_SERVICES env variable or application.yaml or application.properties)
- for Xsuaa
- xsappname
- uaadomain
- clientid
- url
- for Identity service
- domains
- clientid
💡 Example of minimal application configuration application.yml for local setup.
Autoconfiguration for multiple Xsuaa service instance bindings is not available for the Converter bean. You will need to provide it manually. An example can be found here.
- Hybrid Usage
Demonstrates how to leverage
spring-security
library to secure a Spring Boot web application with tokens issued by SAP Identity service or XSUAA. Furthermore, it documents how to implement Spring WebMvcTests usingjava-security-test
library. - Basic Auth Usage
Legacy example that demonstrates how to leverage
spring-security
library to secure a Spring Boot web application with username/password provided via Basic Auth header. Furthermore, it documents how to implement Spring WebMvcTests usingjava-security-test
library. - Webflux Hybrid Usage
Shows how to usespring-security
library with both tokens issued by XSUAA and SAP Identity service in an reactive environment.