Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
monika_palova
Associate
Associate

Getting Started & Why It Matters


Hello, BTP developers!

Today we are going to look at how you can implement your own role providing mechanism from SAP SuccessFactors to an extension application deployed in SAP BTP, Cloud Foundry runtime.
I am sure since you are here, you already know how to extend SAP SuccessFactors using SAP BTP, Cloud Foundry runtime and how to configure Single Sign-On between a subaccount in SAP BTP and SAP SuccessFactors.
If not, take a look at this documentation:

- Extending SAP SuccessFactors in the Cloud Foundry and Kyma Runtime
- Configure Single Sign-On Between a Subaccount in SAP BTP and SAP SuccessFactors

We will be focusing on a specific obstacle you may have encountered after developing and deploying your SAP SuccessFactors extension application in the SAP BTP, Cloud Foundry runtime -
having your user roles sent from SAP SuccessFactors to your CloudFoundry extension application.

At the moment, there is no automatic way to propagate a user's permission roles from your SAP SuccessFactors company to the extension application. This would mean that after authenticating in your application with their SAP SuccessFactors credentials, the users will not have the same permissions as they would in SAP SuccessFactors. This, as you can see, is not the most intuitive behavior. One would expect to have the same roles and permissions.

There are two ways to provide the extension application with the SAP SuccessFactors user roles. First way would be to double maintain the permissions and manually copy the permissions for each user from SAP SuccessFactors to the subaccount in SAP BTP. That, of course, is not the "prettiest" solution. In this tutorial, I am going to show you the second way of achieving this. You can modify your extension application by creating your own SAP SuccessFactors role provider.

Implementing Custom SAP SuccessFactors Role Provider for SAP BTP, Cloud Foundry Runtime


In the following steps, I am going to show you how to implement a role provider for a Java extension application which is implemented with the Spring Framework. It should be similar for other extension applications as well.

So, you have already set up your security and are likely using the security capabilities of the SAP BTP Spring XSUAA Security Library to secure your application. This library will extract the permissions that are in the JSON Web Token (JWT) which is used for the caller's authentication. At this moment, the SAP SuccessFactors identity provider does not send the user roles and permissions to your SAP Authorization and Trust Management service instance and they are not passed to the JWT. This means that you will need a custom JWT Authentication Converter to get the caller's roles from SAP SuccessFactors.

Your current security configuration setup looks like this:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

private final XsuaaServiceConfiguration xsuaaServiceConfiguration;

@Autowired
public SecurityConfiguration(XsuaaServiceConfiguration xsuaaServiceConfiguration) {
this.xsuaaServiceConfiguration = xsuaaServiceConfiguration;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests(authz ->
authz
.requestMatchers("/hello").hasAuthority("Read"))
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new TokenAuthenticationConverter(xsuaaServiceConfiguration));
}
}

The XsuaaServiceConfiguration instance is used to instantiate a class which implements Converter<Jwt, AbstractAuthenticationToken>. This way, the permissions of the user are taken from the subaccount in SAP BTP. In the example above, if the user has the "Read" permission, they will be able to call the /hello path successfully.

In order to use authorities which are defined in the SAP SuccessFactors company and not in the subaccount in SAP BTP, we will need to write our own custom implementation of the Converter<Jwt, AbstractAuthenticationToken> interface.

Here is a simple implementation of such converter:
@Component
public class SfRoleProvider implements Converter<Jwt, AbstractAuthenticationToken> {

private final SfRolesRetriever sfRolesRetriever;

@Autowired
public SfRoleProvider(SfRolesRetriever sfRolesRetriever) {
this.sfRolesRetriever = sfRolesRetriever;
}

@Override
public AbstractAuthenticationToken convert(Jwt jwt) {
return new AuthenticationToken(jwt, getSuccessFactorsAuthorities(jwt));
}

private Collection getSuccessFactorsAuthorities(Jwt jwt) {
return sfRolesRetriever.getRoles(jwt)
.stream()
.map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toSet());
}
}

This class provides an AuthenticationToken in which the authorities are not directly retrieved from the JWT but are retrieved from the SAP SuccessFactors company via the SfRolesRetriever.

SfRolesRetriever makes a call to the SAP OData API to retrieve the roles for the authenticated user, as shown below:
@Component
public class SfRolesRetriever {

private static final String DESTINATION_NAME = "{destination-name}";

private final DestinationServiceConfiguration destinationServiceConfiguration;
private final DestinationServiceAuthorizationHeaderRetriever destinationServiceAuthorizationHeaderRetriever;
private final HttpClient httpClient;

@Autowired
public SfRolesRetriever(DestinationServiceConfiguration destinationServiceConfiguration,
DestinationServiceAuthorizationHeaderRetriever destinationServiceAuthorizationHeaderRetriever) {
this.httpClient = HttpClient.newHttpClient();
this.destinationServiceConfiguration = destinationServiceConfiguration;
this.destinationServiceAuthorizationHeaderRetriever = destinationServiceAuthorizationHeaderRetriever;
}

public Collection getRoles(Jwt jwt) {
JSONObject destination = getDestination(jwt);

String successFactorsURL = destination.getJSONObject("destinationConfiguration").getString("URL");
String successFactorsAccessToken = destination.getJSONArray("authTokens").getJSONObject(0).getString("value");

return getRolesFromSuccessFactors(jwt, successFactorsURL, successFactorsAccessToken);
}

private JSONObject getDestination(Jwt jwt) {
String destinationServiceURL = destinationServiceConfiguration.getUri()
+ "/destination-configuration/v1/destinations/"
+ DESTINATION_NAME;
String authorizationHeader = destinationServiceAuthorizationHeaderRetriever.getAuthorizationHeader();

HttpRequest getDestinationRequest = HttpRequest.newBuilder()
.uri(URI.create(destinationServiceURL))
.header("X-user-token", jwt.getTokenValue())
.header(HttpHeaders.AUTHORIZATION, authorizationHeader)
.GET()
.build();

HttpResponse destinationServiceResponse = null;
try {
destinationServiceResponse = httpClient.send(getDestinationRequest, BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new SfRolesRetrieverException("Failed to retrieve destination", e);
}

return new JSONObject(destinationServiceResponse.body());
}

private Collection getRolesFromSuccessFactors(Jwt jwt, String successFactorsURL,
String successFactorsAccessToken) {
String username = jwt.getClaimAsString(TokenClaims.USER_NAME);

HttpRequest getRolesRequest = HttpRequest.newBuilder()
.uri(URI.create(successFactorsURL + "/odata/v2/getUserRolesByUserId?userId=" + username))
.header(HttpHeaders.AUTHORIZATION, "Bearer " + successFactorsAccessToken)
.header(HttpHeaders.ACCEPT, "application/json")
.GET()
.build();

HttpResponse successFactorsResponse = null;
try {
successFactorsResponse = httpClient.send(getRolesRequest, BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new SfRolesRetrieverException("Failed to retrieve roles from SuccessFactors", e);
}

return parseRolesFromSuccessFactorsResponse(successFactorsResponse.body());
}

private Collection parseRolesFromSuccessFactorsResponse(String successFactorsResponseJSON) {
JSONArray rolesJSONArray = new JSONObject(successFactorsResponseJSON)
.getJSONObject("d")
.getJSONArray("results");

Set roles = new HashSet<>();
for (int i = 0; i < rolesJSONArray.length(); i++) {
String roleName = rolesJSONArray.getJSONObject(i)
.getString("roleName");

roles.add(roleName);
}

return roles;
}
}

SfRolesRetriever uses the destination which was created when you created an SAP SuccessFactors Extensibility service instance to consume the OData API. The value of {destination-name} has to be the same as the value of the generated destination which is the same as the name of the SAP SuccessFactors Extensibility service instance. To get the roles of the authenticated user from SAP SuccessFactors, you will need the SAP SuccessFactors URL and an access token which is generated by the destination. The extraction of the destination data happens in the getDestination(Jwt jwt) method. It uses the DestinationServiceConfiguration and the DestinationServiceAuthorizationHeaderRetriever classes to call the Destination service.

The DestinationServiceConfiguration class contains base information about the Destination service which is taken from the SAP BTP, Cloud Foundry runtime.

DestinationServiceAuthorizationHeaderRetriever creates an authorization header to be used when calling the Destination service.
@Component
public class DestinationServiceAuthorizationHeaderRetriever {

private final DestinationServiceConfiguration destinationServiceConfiguration;
private final UaaContextFactory uaaContextFactory;

@Autowired
public DestinationServiceAuthorizationHeaderRetriever(DestinationServiceConfiguration destinationServiceConfiguration,
UaaContextFactory uaaContextFactory) {
this.destinationServiceConfiguration = destinationServiceConfiguration;
this.uaaContextFactory = uaaContextFactory;
}

public String getAuthorizationHeader() {
String clientId = destinationServiceConfiguration.getClientId();
String clientSecret = destinationServiceConfiguration.getClientSecret();

TokenRequest tokenRequest = constructTokenRequest(clientId, clientSecret);
UaaContext xsuaaContext = uaaContextFactory.authenticate(tokenRequest);

return "Bearer " + xsuaaContext.getToken().getValue();
}

private TokenRequest constructTokenRequest(String clientId, String clientSecret) {
TokenRequest tokenRequest = uaaContextFactory.tokenRequest();
tokenRequest.setGrantType(GrantType.CLIENT_CREDENTIALS);
tokenRequest.setClientId(clientId);
tokenRequest.setClientSecret(clientSecret);

return tokenRequest;
}
}

A TokenRequest that contains all the information about the Destination service client is created. UaaContextFactory authenticates the client and retrieves an access token that can be used later on to call the Destination service. The UaaContextFactory bean is set up in a Spring configuration analogical to this one:
@Configuration
class XsuaaConfiguration {

@Bean
@Primary
XsuaaServiceConfiguration getXSUAAConfig() {
OAuth2ServiceConfiguration config = Environments.getCurrent().getXsuaaConfiguration();

XsuaaServiceConfiguration xsuaaServiceConfiguration = new XsuaaServiceConfiguration();
xsuaaServiceConfiguration.setClientId(config.getClientId());
xsuaaServiceConfiguration.setClientSecret(config.getClientSecret());
xsuaaServiceConfiguration.setUaaDomain(config.getProperty(ServiceConstants.XSUAA.UAA_DOMAIN));
xsuaaServiceConfiguration.setXsAppName(config.getProperty(ServiceConstants.XSUAA.APP_ID));
xsuaaServiceConfiguration.setUrl(config.getUrl().toString());

return xsuaaServiceConfiguration;
}

@Bean
UaaContextFactory getUaaContextFactory(XsuaaServiceConfiguration xsuaaServiceConfiguration)
throws URISyntaxException {
URI xsuaaURI = xsuaaServiceConfiguration.getUrl();

return UaaContextFactory.factory(xsuaaURI);
}
}

We have the Destination service configuration and an authorization header. We can then call the Destination service with them and provide the current user's information in the X-user-token header. That way, when the data is retrieved, the response will contain an SAP SuccessFactors access token generated for the current user.

After we have the destination data, we can retrieve the SAP SuccessFactors URL and an access token with which we can call it. SfRolesRetriever executes the call to the SAP SuccessFactors OData API in the getRolesFromSuccessFactors(Jwt jwt, String successFactorsURL, String successFactorsAccessToken) method. It uses the userId which is extracted from the JWT and calls the OData API to retrieve the roles of the user.

More information about extracting the current user's roles:
- How to fetch the Roles assigned to a User using OData API
- API Reference for the getUserRolesByUserId call

SfRoleProvider maps the retrieved roles to a collection of authorities which are then used to authorize the API caller, as described in the new and changed security configuration:
@Configuration
@EnableWebSecurity
class SecurityConfiguration {

private final SfRoleProvider sfRoleProvider;

@Autowired
SecurityConfiguration(SfRoleProvider sfRoleProvider) {
this.sfRoleProvider = sfRoleProvider;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/hello").hasAuthority("SfCustomRole"))
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(sfRoleProvider);

return http.build();
}
}

Now, the user can call successfully the /hello API path only if they have the "SfCustomRole" role added in the SAP SuccessFactors company.

You can find more information on how you can manage role-based permissions in SAP SuccessFactors at:
- Using Role-Based Permissions
- Managing security using SAP SuccessFactors Role-Based Permissions (RBP)

Summary of the New Functionality


You now have your own custom SAP SuccessFactors role provider, which retrieves your users' roles directly from the role-based permissions section in your SAP SuccessFactors company.
Your extension application has become more intuitive to its users since they authenticate with their SAP SuccessFactors credentials and have the same roles and permissions as they would have in the SAP SuccessFactors company directly.