cancel
Showing results for 
Search instead for 
Did you mean: 

Technical user to access API of CAP application on BTP

Moritz__
Explorer
0 Kudos

Hello,

we are developing an ODATA services using the Java CAP framework. This ODATA services needs to be accessed by an external (not deployed on BTP) application in a secure way.

 We bound a XSUAA service to our API and integrated the resource server package.

		<dependency>
			<groupId>com.sap.cloud.security</groupId>
			<artifactId>resourceserver-security-spring-boot-starter</artifactId>
			<version>3.3.5</version>
		</dependency>

 Additionally we setup an approuter and are routing all requests on a specific path to our API and set the authentication type to "xsuaa".

As a user I am requested to login using the SSO and everything works as expected.

Now we need access to the API for an external system. I would call this a technical user.

  • What are the concepts on the BTP for a case like this?
  • Can we reuse the bound xsuaa service?
  • How can we create such a user?

I am new to the BTP and grateful for any feedback and hints!

Thank you!

Accepted Solutions (1)

Accepted Solutions (1)

Ivan-Mirisola
Product and Topic Expert
Product and Topic Expert

Hi @Moritz__,

Go to the instances/subscription and open the xsuaa instance that is bound to your application. Then create a service key for it.

Then inspect the Service Key to retrieve the OAuth2 information that is required for your external call to be made. Namely:

  • Client ID
  • Client Secret
  • Token URL (don't forget to add the suffix - usually it is "/oauth/token")
  • Service URL

The you make a "client credentials" request to the token url to fetch a JWT that you can then forward as Authentication method for your service.

Best regards,
Ivan

Moritz__
Explorer
0 Kudos
Moritz__
Explorer
0 Kudos
Hello @Ivan-Mirisola, thank you for your answer. This looks fairly easy. We would need to add roles to this user and I found this help page https://help.sap.com/docs/service-manager/sap-service-manager/creating-service-keys-in-cloud-foundry. On the app itself I only see service bindings, I guess you mean the to create the key on the bound xsuaa service? While the creation of that, I can somehow pass the roles as json configuration?
Ivan-Mirisola
Product and Topic Expert
Product and Topic Expert
0 Kudos

Hi @Moritz__ ,

Sorry for the confusion. You are correct, instead of the deployed application, go to the instances/subscription and open the xsuaa instance that is bound to your application. Then create a service key for it.

Best regards,
Ivan

Moritz__
Explorer
0 Kudos

Hello @Ivan-Mirisola,

great! Meanwhile I already tried it with postman and it is working well.

One additional question: How can I add a role/scope to the token? I would like to use role based access control on the odata service and allow this service key only to access specific endpoints. I guess it can be done while the key creation?

Best regards,

Moritz

Ivan-Mirisola
Product and Topic Expert
Product and Topic Expert
0 Kudos

Hi @Moritz__,

Roles are defined on the xs-security.json file used when you've created the xsuaa instance.

NOTE: AFAIK, once you create the xsuaa instance, you cannot update it with a new xs-security.json. You must recreate the instance to do it.

Once you've created, you can check the roles present on the JWT token by using the annotation '@AuthenticationPrincipal' like so on any controller's method:

 

   @GetMapping(path = "")
   public ResponseEntity<String> readAll(@AuthenticationPrincipal Token token) {
       if (!token.getAuthorities().contains(new SimpleGrantedAuthority("Display"))) {
           throw new NotAuthorizedException("This operation requires \"Display\" scope");
       }
       return new ResponseEntity<String>("Hello World!", HttpStatus.OK);
   }
}

 

Of via SecurityConfig.java using antmatchers like so:

 

import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
import com.sap.cloud.security.xsuaa.token.TokenAuthenticationConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfig {

	@Autowired
	XsuaaServiceConfiguration xsuaaServiceConfiguration;

	@Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                .sessionManagement(management -> management
                        // session is created by approuter
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // demand specific scopes depending on intended request
                .authorizeRequests(requests -> requests

                        .antMatchers("/**").authenticated()
                        .anyRequest().denyAll())
                .oauth2ResourceServer(server -> server.jwt()
                        .jwtAuthenticationConverter(getJwtAuthoritiesConverter()));

        return http.build();
    }

	/**
	 * Customizes how GrantedAuthority are derived from a Jwt
	 *
	 * @returns jwt converter
	 */
	Converter<Jwt, AbstractAuthenticationToken> getJwtAuthoritiesConverter() {
		TokenAuthenticationConverter converter = new TokenAuthenticationConverter(xsuaaServiceConfiguration);
		converter.setLocalScopeAsAuthorities(true);
		return converter;
	}

}

 

Best regards,
Ivan

Moritz__
Explorer
0 Kudos

Hello @Ivan-Mirisola,

Thank you for the code. I already had something similar in place. I am just using the CdsReadEventContext of the odata service event handlers and retrieve the user information.

    @On(event = CqnService.EVENT_READ, entity = "DebugService.User")
    public void user(CdsReadEventContext context){
        UserInfo userInfo = context.getUserInfo();
        boolean isSystemUser = userInfo.isSystemUser();
        String username = userInfo.getName();
        List<String> roles = Arrays.asList(userInfo.getRoles().toArray(new String[]{}));

        ArrayList<HashMap<String, Object>> resultList = new ArrayList();
        HashMap<String, Object> resultMap = new HashMap<>();
        resultMap.put("username", username);
        resultMap.put("isSystemUser", isSystemUser);
        resultMap.put("roles", roles);
        resultList.add(resultMap);
        context.setResult(resultList);

    }

 I created roles via the xs-security.json and assigned them via the BTP cockpit. It is working fine and opening the above odata endpoint, gives me the below output for my user:

{
    "@context": "$metadata#User",
    "@metadataEtag": "...",
    "value": [
        {
            "username": "<my-user>",
            "isSystemUser": false,
            "roles": [
                "openid",
                "Dummy_Viewer",
                "DEBUG"
            ]
        }
    ]
}

 I called it as well with the token generated by the service key:

{
    "@context": "$metadata#User",
    "@metadataEtag": "...",
    "value": [
        {
            "username": "system-internal",
            "isSystemUser": true,
            "roles": [
                "uaa.resource"
            ]
        }
    ]
}

 How do I assign a role like "DEBUG" or "Dummy_Viewer" to the service key?

By the way you can apply changes to the xs-security.json with this command:

cf update-service <xsuaa-service> -p application -c xs-security.json

Thank you and best regards,
Moritz

Ivan-Mirisola
Product and Topic Expert
Product and Topic Expert
0 Kudos

Hi @Moritz__,

Thanks for letting me know you can apply updates on the xsuaa instance via CLI. I remember that this was not possible and currently the cokpit application does allow you to do this but it has always failed on me while trying to update an instance of xsuaa.

I am not sure I quite understand the question about debug or dummy viewer. 

Are you asking how you are able to "protect" the Service Key information? 

If so, the service key is protected the same way all other development artifacts are in the space. If you are a Developer of that space, you will be able to create and view it. If you are an Auditor or a Debugger, then you are able to do less stuff on BTP. 

BTP Roles are explained here:

https://help.sap.com/docs/btp/sap-business-technology-platform/about-roles-in-cloud-foundry-environm...

Best regards,
Ivan

Moritz__
Explorer
0 Kudos

Hello @Ivan-Mirisola,

No, it is not about protecting the key on the BTP.
The API is using role based access control to protect endpoints.
In the CDS model we can defined required roles for an entity. These can be custom roles like the ones I made up (debug, dummy_viewer). Here is some example cds model from the java cap documentation.

 

service BooksService @(requires: 'any') {
  @readonly
  entity Books @(requires: 'any') {...}

  entity Reviews {...}

  entity Orders @(requires: 'Customer') {...}
}

 

 Let's go through step by step:

1. We generate the service key
2. We provide the client secret and client id along the authentication url to a third party system.
3. The third party system uses these information to retrieve a token.
4. They use this token to send a request to our api.

Now depending on our required roles or probably antMatchers from our security configuration some cases are possible:

Case 1:
- API is configured to allow all requests when authenticated
- antMatchers("/**").authenticated().anyRequest().denyAll()
- cds model annotations all are @(requires: 'any')
- The API will respond to the API call (step 4) with status 200 ok, because the request is authenticated and no special authorization by a role is required.

Case 2:
- API is configured to allow request on a specific path to require a role and all others to be authenticated
- .requestMatchers("/**").hasAuthority("ROLE").anyRequest().denyAll()
- cds model annotations all are @(requires: 'ROLE')
- The API will respond to the API call with status 403 forbidden, because the token is missing the role/authority ROLE
- The token only has the role uaa.resource

The question is: How do you manage, that the token requested with a specific service key includes one or more defined roles? Like ROLE, Customer, Debug, Dummy Viewer...

I prepared a number of steps, you can run in the Business Application Studio. It will retrieve the service key and use it to request a token. This token is then decoded. Tools like curl and jq are already installed there.
A cf login is required.

 

# define your xsuaa service name and the name of the key
xsuaaservice=<put your xsuaa service name here>
keyname=<put your key name here>

# check the values
echo $xsuaaservice
echo $keyname

# get your key and save it to test-key.json. The tail command removes some non-json debug output
cf service-key $xsuaaservice $keyname | tail -n +2 > test-key.json

# view the key
cat test-key.json

# extract required values
url=$(cat test-key.json | jq -r .credentials.url)
clientid=$(cat test-key.json | jq -r .credentials.clientid)
clientsecret=$(cat test-key.json | jq -r .credentials.clientsecret)

# view extracted values
echo $url
echo $clientid
echo $clientsecret

# request a token and save to token-response.json
curl --location --request POST "${url}/oauth/token?Accept=application/json;charset=utf8&Content-Type=application/x-www-form-urlencoded" \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode "client_id=${clientid}" \
--data-urlencode "client_secret=${clientsecret}" \
--data-urlencode 'response_type=token' > token-response.json

#view the token
cat token-response.json

# view the scope
# this outputs "uaa.resource"
# here I need additional scopes
cat token-response.json | jq .scope

# The access token is part of the response
cat token-response.json | jq -r .access_token

# You can stop here or further analyze the token ;)
# Anyway consider the last cleanup step

# The scope is included in the access token itself.
# A token has tree parts: header, payload and the signature
# Each part is base64 encoded and these are then concatenated by a dot to build the token
# The scope is part of the payload. The payload is in json format.
# You can split the token into its three parts (saved to variable access_token_parts):
IFS='.' read -ra access_token_parts <<< "$(cat token-response.json | jq -r .access_token)"
# You can check the outputs, but it is encoded
echo "${access_token_parts[1]}"
# decode an format the token payload
echo "${access_token_parts[1]}" | base64 --decode | jq
# if you are just interested in the scope
echo "${access_token_parts[1]}" | base64 --decode | jq .scope

# When you are done, you might want to cleanup the secrets
# remove created files
rm test-key.json
rm token-response.json
# unset variables
unset xsuaaservice
unset keyname
unset url
unset clientid
unset clientsecret
unset access_token_parts

 

Decoded token payload with some redacted parts:

{
  "jti": "<some id>",
  "ext_attr": {
    "enhancer": "XSUAA",
    "subaccountid": "<uuid>",
    "zdn": "<subaccount name>"
  },
  "sub": "<clientid>",
  "authorities": [
    "uaa.resource"
  ],
  "scope": [
    "uaa.resource"
  ],
  "client_id": "<clientid>",
  "cid": "<clientid>",
  "azp": "<clientid>",
  "grant_type": "client_credentials",
  "rev_sig": "...",
  "iat": 1712222943,
  "exp": 1712266143,
  "iss": "<url>/oauth/token",
  "zid": "<some id>",
  "aud": [
    "uaa",
    "<clientid>"
  ]
}

I hope, that clarifies it a bit and does not confuse too much.

Thank you and best regards,

Moritz 

Ivan-Mirisola
Product and Topic Expert
Product and Topic Expert
0 Kudos

Hi @Moritz__,

It is clear to me now what you are trying to achieve.

And I believe this is very easy to do and there are some options here:

1) Client Credentials Flow

What you want to achieve can be done using the Technical Authentication under the OAuth2 Client Credentials flow:

https://help.sap.com/docs/workflow-capability/workflow-cloud-foundry/technical-authentication

By specifying the "authorities" clause on your xs-security.json, your xsuaa instance will grant specific scopes on the fetched token.

If you carefully read the note provided by the documentation you will realize that you may need more than an 'xsuaa' instance for front-end users.
You will also need another instance of 'xsuaa' for each granular profile needed for API calls. The additional xsuaa instance will be bound solely to the Java application providing the API services.

And, since each instance will have a service key assigned to them, that means that a "Client Credential" flow will give you a set of specific Scopes - according to what is specified on your 'xs-security.json'. 

srv(Java) --- bound ---> xsuaa-api ---> xs-security-api.json (authorities= scope:debug)
===> grants debug scope to token fetched using client id and secret from a service key created under the xxuaa-api instance. 

app(App Router) --- bound ---> xsuaa ---> xs-security.json (no authorities defined)
===> grants scopes according to role collections defined in BTP and assigned to users. 

2) Password Authentication Flow

Another way of doing the same with a single instance of xsuaa would be to use the Oauth2 Password authentication:

https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/oauth-password-authentication

This means that right after validating the client credentials, the token will be emitted for an 'actual' user with basic authentication.
Which in turn will get all the scopes a regular user gets.

The only caveat of this approach is that user passwords have to be changed on a regular basis for security reasons.
And it will become hard to maintain that password in sync with the external API callers.

3) Use API Management instead

API Management will give you other means to consume the API which allows you to "hide" the scope logic determination based on a set of rules to be applied by you on an API flow. This means that your API will now be under the control of 'API Management' and you will simply give 'clients' and 'APIKey' to gain access to the API. APIKeys are generated on API Management by you for each API you expose. So you can have control over who is accessing what. That means you are able to control which customer is using each API. You can monitor how many times each API is being called by each customer and then monetize it according to your rules. 

It is a completely different approach where you will basically work the same way as many public API providers work out there.

Best regards,
Ivan

 

 

Answers (0)