This migration guide is a step-by-step guide explaining how to replace the following SAP-internal Java Container Security Client libraries
- com.sap.xs2.security:java-container-security
- com.sap.cloud.security.xsuaa:java-container-security
with this open-source version.
Please note, that this Migration Guide is NOT intended for applications that leverage token validation and authorization checks using SAP Java Buildpack. This sample showcases the setup when using SAP Java Buildpack.
The following list serves as an overview of this guide and points out sections that describe required code changes for migrating applications.
- Spring 5 is required! See section Prerequisite.
- Maven dependencies need to be changed. See section Maven Dependencies.
- The Spring Security configuration needs changes described in section Configuration changes.
Token
instead ofXSUserInfo
. See section Fetch data from token.- If your application has multiple XSUAA bindings, see section Multiple bindings.
If your application does not already use Spring 5 you need to upgrade to Spring 5 first to use spring-xsuaa. Likewise if you use Spring Boot make sure that you use Spring Boot 2.
Your application is probably using Spring Security OAuth 2.x which is being deprecated with Spring 5. The successor is Spring Security 5.2.x but it is not compatible with Spring Security OAuth 2.x. See the official migration guide for more information.
We already migrated the cloud-bulletinboard-ads application. You can take a look at this commit which shows what had to be changed to migrate our open-SAP course application from Spring 4 to Spring 5.
💡 Please also consider the spring-xsuaa requirements.
To use the new spring-xsuaa client library the dependencies declared in maven pom.xml
need to be changed.
See the documentation on what to add to your pom.xml
.
Now you are ready to remove the java-container-security
client library by deleting the following dependencies from the pom.xml:
groupId (deprecated) | artifactId (deprecated) |
---|---|
com.sap.xs2.security | java-container-security |
com.sap.xs2.security | api |
com.sap.cloud.security.xssec | api |
com.sap.cloud.security.xsuaa | java-container-security-api |
com.sap.cloud.security.xsuaa | java-container-security |
com.sap.cloud.security.xsuaa | api |
Note: The dependency
com.sap.cloud.security.xsuaa:api
should be removed as well, asspring-xsuaa
provides it already as transitive dependency.
Furthermore, make sure that you do not refer to any other sap security library with groupId com.sap.security
or com.sap.security.nw.sso.*
.
After the dependencies have been changed, the spring security configuration needs some adjustments as well.
One difference between java-container-security
and spring-xsuaa is that spring-xsuaa
does not provide the SAPOfflineTokenServicesCloud
class. This is because SAPOfflineTokenServicesCloud
requires
Spring Security OAuth 2.x which is being deprecated in Spring 5.
This means that you have to remove the SAPOfflineTokenServicesCloud
bean from your security configuration
and adapt the HttpSecurity
configuration. This involves the following steps:
- The
@EnableResourceServer
annotation must be removed. Instead, the resource server has to be configured using the Spring Security DSL syntax.
See the docs for an example configuration. - The
antMatchers
must be configured to check against the authorities. For this theTokenAuthenticationConverter
needs to be configured like described in the docs. Note: with the removal of the deprecated Spring Security OAuth library the web expressionaccess("#oauth2.hasScope('" + xsAppName + ".Display"’)")
has been removed, and must be replaced withhasAuthority("Display")
.
We already added spring-xsuaa
and java-security-test
to the cloud-bulletinboard-ads application and
this commit
shows the security relevant parts.
There are two options to access information of the XSUAA service instance (VCAP_SERVICES
credentials):
- Via Spring
@Value
@Value("${xsuaa.xsappname}")
String xsAppName;
- Via XsuaaServiceConfiguration bean
@Autowired
XsuaaServiceConfiguration xsuaaServiceConfiguration;
...
xsuaaServiceConfiguration.getAppId();
There is no need to configure SAP_JWT_TRUST_ACL
within your deployment descriptor such as manifest.yml
.
Instead the Xsuaa service instance adds audiences to the issued JSON Web Token (JWT) as part of the aud
claim.
Whether the token is issued for your application or not is now validated by the XsuaaAudienceValidator
.
This comes with a change regarding scopes. For a business application A that wants to call an application B, it's now mandatory that the application B grants at least one scope to the calling business application A. You can grant scopes with the xs-security.json
file. For additional information, refer to the Application Security Descriptor Configuration Syntax, specifically the sections referencing the application and authorities.
You can skip this section, in case your application is bound to only one Xsuaa service instance. The xsuaa-spring-boot-starter
does not support multiple XSUAA bindings of plan application
and broker
. The Xsuaa service instance of plan api
get always ignored.
In case of multiple bindings you need to adapt your Spring Security Configuration as following:
- You need to get rid of
XsuaaServicePropertySourceFactory
, becauseXsuaaServicesParser
raises the error.
- In case you make use of
xsuaa-spring-boot-starter
Spring Boot starter, you need to disable auto-configuration within your*.properties
/ or*.yaml
file:spring.xsuaa.multiple-bindings = true
- Or, make sure that your code does not contain
@PropertySource(factory = XsuaaServicePropertySourceFactory.class, value = {""})
-
Instead, provide your own implementation of
XsuaaSecurityConfiguration
interface to access the primary Xsuaa service configuration of your application (chose the service instance of planapplication
here), which are exposed in theVCAP_SERVICES
system environment variable (in Cloud Foundry). As of version2.6.2
you can implement it like that:import com.sap.cloud.security.xsuaa.XsuaaCredentials; import com.sap.cloud.security.xsuaa.XsuaaServiceConfigurationCustom; ... @Bean @ConfigurationProperties("vcap.services.<<name of your xsuaa instance of plan application>>.credentials") public XsuaaCredentials xsuaaCredentials() { return new XsuaaCredentials(); // primary Xsuaa service binding, e.g. application } @Bean public XsuaaServiceConfiguration customXsuaaConfig() { return new XsuaaServiceConfigurationCustom(xsuaaCredentials()); }
-
You need to overwrite
JwtDecoder
bean so that theAudienceValidator
checks the JWT audience not only against the client id of the primary Xsuaa service instance, but also of the binding of planbroker
. As of version2.6.2
you can implement it like that:@Bean @ConfigurationProperties("vcap.services.<<name of your xsuaa instance of plan broker>>.credentials") public XsuaaCredentials brokerCredentials() { return new XsuaaCredentials(); // secondary Xsuaa service binding, e.g. broker } @Bean public JwtDecoder getJwtDecoder() { XsuaaCredentials brokerXsuaaCredentials = brokerCredentials(); XsuaaAudienceValidator customAudienceValidator = new XsuaaAudienceValidator(customXsuaaConfig()); // customAudienceValidator.configureAnotherXsuaaInstance("test3!b1", "sb-clone1!b22|test3!b1"); customAudienceValidator.configureAnotherXsuaaInstance(brokerXsuaaCredentials.getXsAppName(), brokerXsuaaCredentials.getClientId()); return new XsuaaJwtDecoderBuilder(customXsuaaConfig()).withTokenValidators(customAudienceValidator).build(); }
-
For authorization checks you can't perform any longer "local scope checks", as same scope names might be specified in context of different XSUAA service instances. That means you have to compare scope as it is given with the access token (
scope
claim) including thexsappname
prefix. So, make sure thatTokenAuthenticationConverter
is NOT configured to check for local scopes (setLocalScopeAsAuthorities(false)
)! In this case configure the HttpSecurity with anantMatcher
for local scope "Read" as following:.requestMatchers("/v1/sayHello").hasAuthority(customXsuaaConfig().getAppId() + '.' + "Read")
You may have code parts that requests information from the access token using XSUserInfo userInfo = SecurityContext.getUserInfo()
, like the user's name, its tenant, and so on. So, look up your code to find its usage, for example:
import com.sap.xs2.security.container.SecurityContext;
import com.sap.xs2.security.container.UserInfo;
import com.sap.xs2.security.container.UserInfoException;
try {
XSUserInfo userInfo = SecurityContext.getUserInfo();
String logonName = userInfo.getLogonName();
} catch (UserInfoException e) {
// handle exception
}
and replace this with
import com.sap.cloud.security.xsuaa.token.SpringSecurityContext;
import com.sap.cloud.security.xsuaa.token.Token;
Token token = SpringSecurityContext.getToken(); // throws AccessDeniedException
Note 1️⃣: There is no
UserInfo
anymore. To obtain the token from the thread local storage, you have to use Spring's Security Context managed by theSecurityContextHolder
. This is explained in detailed in the usage section.
Note 2️⃣: In case you have used formerly
Principal.getName()
be aware thatspring-xsuaa
returns a user name or client id in the following format:
user/<origin>/<logonName>
client/<clientid>
See also Github issue #399.
Unlike XSUserInfo
interface there is no XSUserInfoException
raised, in case the token does not contain the requested claim. You can check the interface, whether it can also return a Nullable
. Then you can either perform a null check or check in advance, whether the claim is provided as part of the token, e.g. Token.hasClaim(TokenClaims.CLAIM_CLIENT_ID)
.
The XSUserInfo
interface provides some special methods that are not available in
the com.sap.cloud.security.xsuaa.token.Token
.
See the following table for methods that are not available anymore and workarounds.
XSUserInfo method | Workaround in spring.xsuaa |
---|---|
checkLocalScope |
Adapt the default behaviour of TokenAuthenticationConverter.setLocalScopeAsAuthorities(true) to let getAuthorities return local scopes. E.g. token.getAuthorities().contains(new SimpleGrantedAuthority("Display")) |
checkScope |
Use getScopes and check if the scope is contained. |
getAttribute |
Use getXSUserAttribute . |
getDBToken |
Not implemented. |
getHdbToken |
Not implemented. |
getIdentityZone |
Use getZoneId to get the tenant GUID or use getSubaccountId to get subaccount id, e.g. to provide it to the metering API. |
getJsonValue |
Use containsClaim and getClaimAsString . See section XsuaaToken. |
getSystemAttribute |
This extracts data from xs.system.attributes claim. See section XsuaaToken. |
getToken |
Not implemented. |
hasAttributes |
Use getXSUserAttribute and check of the attribute is available. |
isInForeignMode |
Not implemented. |
requestToken |
Deprecated in favor of XsuaaTokenFlows which is provided with token-client library. You can find a migration guide here. |
requestTokenForClient |
Deprecated in favor of XsuaaTokenFlows which is provided with token-client library. You can find a migration guide here. |
The runtime type of Token
is XsuaaToken
. XsuaaToken
provides additional
methods that can be used to extract data from the token since it is a subclass of
org.springframework.security.oauth2.jwt.Jwt
. So you can for example read
claims with getClaim
or check for claims with containsClaim
. See the
spring documentation
for more details.
In your unit test you might want to generate jwt tokens and have them validated. The new
java-security-test library provides it's own JwtGenerator
. This can be embedded using the SecurityTestRule
in Junit 4. See the following snippet as example:
@ClassRule
public static SecurityTestRule securityTestRule =
SecurityTestRule.getInstance(Service.XSUAA)
.setKeys("/publicKey.txt", "/privateKey.txt");
Using the SecurityTestRule
you can use a pre configured JwtGenerator
to create JWT tokens with custom scopes for your tests. It configures the JwtGenerator in such a way that it uses the public key from the publicKey.txt
file to sign the token.
String jwt = securityTestRule.getPreconfiguredJwtGenerator()
.withAppId(SecurityTestRule.DEFAULT_APP_ID)
.withLocalScopes("Display", "Update")
.createToken()
.getTokenValue();
See the java-security-test documentation for more details, also on how to leverage JUnit 5 extensions.
For local testing you might need to provide custom VCAP_SERVICES
before you run the application.
The new security library requires the following key value pairs in the VCAP_SERVICES
under xsuaa/credentials
for jwt validation:
"uaadomain" : "localhost"
"verificationkey" : "<public key your jwt token is signed with>"
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.createToken()
and run yourJUnit
tests to fetch the value ofjwt
from there. In that case you can use the publicKey fromjava-security-test
, like its done here.
Now you can test the service manually in the browser using the Postman
chrome plugin and check whether the secured functions can be accessed when providing a valid generated Jwt Token.
When your code compiles again you should first check that all your unit tests are running again. If you can test your application locally make sure that it is still working and finally test the application in cloud foundry.
In case you face issues to apply the migration steps check this troubleshooting for known issues and how to file the issue.