SpringBoot Microservices using PCF Tiles SCS and Single Sign-On

OAuth 2.0 Actors

  • Application: A client that makes protected requests using the authorization of the resource owner.
  • Authorization Server: The Single Sign-On server that issues access tokens to client applications after successfully authenticating the resource owner.
  • Resource Server: The server that hosts protected resources and accepts and responds to protected resource requests using access tokens. Applications access the server through APIs.

Pre-requisite

  1. First, create the Single Sing-On service from the Marketplace Marketplace Screenshot. Go with the default options and create the service
  2. From the cf cli you can grab the URL for this newly created service as follows SCREENSHOT
  3. When you come in for the first time to the above URL, there will be no Apps or Resources
  4. Now, let’s create resources SCREENSHOT
  5. After resources, let’s create a Service-to-Service App. Ensure to select the pivotal resources (or the resource created in the above step 4.) SCREENSHOT Note: Download the App ID and App Secret locally as you can copy it only the first time. Next time, you will be able to re-generate new App Secret. Copy the OAuth Token URL as well. SCREENSHOT

Scenario 1: Not using SSO Tile in PCF

  1. In this scenario, we do not bind the customer-service to the PCF SSO Tile. Rather, we only configure the following properties in the application.yml and secure the micro-service

     AUTH_SERVER: https://pivot-aparthasarathy.login.run.pcfbeta.io/
     security:
     oauth2:
         resource:
         preferTokenInfo: false
         userInfoUri: ${AUTH_SERVER}/userinfo
         tokenInfoUri: ${AUTH_SERVER}/check_token
         jwk:
             key-set-uri: ${AUTH_SERVER}/token_keys
    
  2. In our java code below, we make use of EnableResourceServer as well as, set a resourceId explicitily. This ensures that, apart from checking the validity of the JWT Token, we also require the token to have the required scope.

     @Configuration
     @Profile({"sso"})
     @EnableResourceServer
     public class AuthConfig {
         @Value("${ssoScope:pivotal}")
         private String ssoResourceId;
    
         @Bean
         public GlobalMethodSecurityConfiguration globalMethodSecurityConfiguration() {
             return new GlobalMethodSecurityConfiguration() {
                 @Override
                 protected MethodSecurityExpressionHandler createExpressionHandler() {
                     return new OAuth2MethodSecurityExpressionHandler();
                 }
             };
         }
    
         @Bean
         public RequestInterceptor requestInterceptor(){
             return new RequestInterceptor() {
                 @Override
                 public void apply(RequestTemplate requestTemplate) {
                     OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)
                             SecurityContextHolder.getContext().getAuthentication().getDetails();
                     requestTemplate.header("Authorization", "bearer " + details.getTokenValue());
                 }
             };
         }
    
         @Bean
         public ResourceServerConfigurer resourceServerConfigurerAdapter() {
             return new ResourceServerConfigurerAdapter() {
                 @Override
                 public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
                     resources.resourceId(ssoResourceId);
                 }
    
                 @Override
                 public void configure(HttpSecurity http) throws Exception {
                     http.addFilterAfter(new OncePerRequestFilter() {
                         @Override
                         protected void doFilterInternal(HttpServletRequest request,
                                                         HttpServletResponse response, FilterChain filterChain)
                                 throws ServletException, IOException {
                             filterChain.doFilter(request, response);
                         }
                     }, AbstractPreAuthenticatedProcessingFilter.class);
                     http.csrf().disable();
                     http.authorizeRequests().anyRequest().authenticated();
                 }
             };
         }
    
  3. With this our app should be secured. If we invoke an API without valid token, you should see 401 Unauthorized response from the app.
  4. First let’s generate the token Ensure jq is installed.

     curl -s -X POST https://pivot-aparthasarathy.login.run.pcfbeta.io/oauth/token -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=f17dfc29-7c0f-4e50-af2c-06a21b670fd0&client_secret=7c7aafa6-18ff-4d37-9e20-3a7dd34d975a&grant_type=client_credentials" | jq -r .access_token
    
  5. If you don’t have jq, you can use Postman SCREENSHOT
  6. Pass this access_token in the Header when invoking /customers API SCREENSHOT
  7. If you pass a token without pivotal resource id, you would receive a 403 Forbidden with a body

     {
         "error": "access_denied",
         "error_description": "Invalid token does not contain resource id (pivotal)"
     }
    

Scenario 2: Using SSO Tile in PCF

  1. In this scenario, we will bind the account-service to the PCF SSO Tile. There is nothing related to security.oauth2 properties in the application.yml rather, those properties get injected through VCAP as we bind to the sso service in the manifest.yml

     ---
     applications:
     - name: account-service
     memory: 1024M
     path: ../../../build/libs/account-service-0.0.1-SNAPSHOT.jar
     host: ani-account-service
     instances: 1
     services:
     - config
     - registry
     - sso
     env:
         SPRING_PROFILES_ACTIVE: sso
         GRANT_TYPE: client_credentials
         SSO_SCOPES: pivotal.write, pivotal.read, spring.write, spring.read
         SSO_AUTHORITIES: pivotal.write, pivotal.read, spring.write, spring.read
         SSO_ACCESS_TOKEN_LIFETIME: 30
    
  2. In our java code below, we make use of EnableResourceServer. Please refer customer-service java code above for this.
  3. Once we generate the token (refer to customer-service section above), we could invoke any REST API and validate the same. SCREENSHOT

Scenario 3: 3rd Party micro-service to call above secured micro-services

Let’s say we have another micro-service bff-service which calls both account-service and customer-service. To make it even more interesting, we could have 2 cases

  • Case 1: bff-service authorization token cannot be re-used because it doesn’t have the required scope to call other microservices
  • Case 2: bff-service is secured by some other type of security (for e.g. ACL based security)

In both the above cases, we need to generate a token through the application, embed that in the HttpHeader before making a RESTTemplate call. In this example, I have used Ribbon with Eureka to invoke the call to ther microservices registered with Eureke

  1. First generate accessToken inside the code. This requires a valid client_id and client_secret. This is where we would use the values configured in the Pre-requisite section above.

     @Service
     @Slf4j
     public class TokenService {
    
         @Autowired
         RestTemplate restTemplate;
    
     /**
      * Get Access Token
      * @return TokenInfo
      */
     public TokenInfo getAccessToken() {
         log.info("Came inside getAccessToken()");
         HttpHeaders headers = getHeaders();
         MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
         map.add("client_id", oAuth2ClientConfig.getClientId());
         map.add("client_secret", oAuth2ClientConfig.getClientSecret());
         map.add("grant_type", oAuth2ClientConfig.getGrantType());
    
         HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers);
         ResponseEntity<TokenInfo> tokenInfoResponseEntity = restTemplate.postForEntity(
                 oAuth2ClientConfig.getAccessTokenUri(), request, TokenInfo.class);
         if (Objects.nonNull(tokenInfoResponseEntity)) {
             return tokenInfoResponseEntity.getBody();
         } else {
             log.error("No Access Token");
             return null;
         }
     }
    
     private HttpHeaders getHeaders() {
         HttpHeaders headers = new HttpHeaders();
         headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
         return headers;
     }
    
  2. Using the above method, we can use this generated token while RESTTemplate calls to other microservices

     @Service
     @Slf4j
     public class BFFService {
    
         @Autowired
         private RestTemplate restTemplate;
    
         @Autowired
         private TokenService tokenService;
    
         @Value("${CUSTOMER_SERVICE_URL}")
         private String URL_CUSTOMER_SERVICE;
    
         @Value("${ACCOUNT_SERVICE_URL}")
         private String URL_ACCOUNT_SERVICE;
    
         /**
         * BackEnd For Front-End method to aggregate data from 2 micro-services
         * @param customerNumber
         * @return
         */
         public UIResponse findCustomerDetailsByNumber(int customerNumber) {
             log.info("Came inside findCustomerDetailsByNumber for customerId: " + customerNumber);
             StopWatch stopWatch = new StopWatch();
             stopWatch.start();
    
             UIResponse uiResponse = null;
    
             log.info("Querying the customer microservice...");
             HttpHeaders httpHeaders = tokenService.getHeadersWithAccessToken();
             HttpEntity<String> entity = new HttpEntity<String>("parameters", httpHeaders);
    
             String uri = URL_CUSTOMER_SERVICE + customerNumber;
             log.info("Calling URI: " + uri);
             ResponseEntity<Customer> customerResponseEntity = restTemplate.exchange(
                     uri, HttpMethod.GET, entity, Customer.class);
             stopWatch.stop();
    
             if (Objects.nonNull(customerResponseEntity)) {
                 Customer customer = customerResponseEntity.getBody();
                 log.info("customer micro-service took " + stopWatch.getTotalTimeMillis() + " milliseconds");
                 log.info("customer micro-service returned " + customer.toString());
    
                 uiResponse = new UIResponse(customer.getFirstName(), customer.getLastName(), customer.getAccountNumber());
                 uiResponse.setContact(new Contact(customer.getCellPhone(), customer.getLandLine(),
                         customer.getStreetAddress(), customer.getCity(), customer.getState(), customer.getZip()));
    
                 //Now make Account service call
                 stopWatch = new StopWatch();
                 stopWatch.start();
                 uri = URL_ACCOUNT_SERVICE + customerNumber;
                 log.info("Calling URI: " + uri);
                 ResponseEntity<List<Account>> accountResponseEntity = restTemplate.exchange(
                         uri, HttpMethod.GET, entity, new ParameterizedTypeReference<List<Account>>() {});
                 stopWatch.stop();
                 log.info("account micro-service took " + stopWatch.getTotalTimeMillis() + " milliseconds");
    
                 if (Objects.nonNull(accountResponseEntity)) {
                     List<Account> accounts = accountResponseEntity.getBody();
                     log.info("account micro-service returned " + accounts.toString());
                     uiResponse.setAccounts(accounts);
                 }
             }
    
             return uiResponse;
         }
    
  3. Of-course the above client could also be done in a simpler way using OAuth2RestTemplate. Create the required bean

     @Bean
     public DefaultOAuth2ClientContext oauth2ClientContext() {
         return new DefaultOAuth2ClientContext(new DefaultAccessTokenRequest());
     }
    
     @Autowired
     OAuth2ClientConfig auth2ClientConfig;
    
     @Bean
     public ClientCredentialsResourceDetails client() {
         ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
         resourceDetails.setClientId(auth2ClientConfig.getClientId());
         resourceDetails.setClientSecret(auth2ClientConfig.getClientSecret());
         resourceDetails.setAccessTokenUri(auth2ClientConfig.getAccessTokenUri());
         resourceDetails.setGrantType(auth2ClientConfig.getGrantType());
         return resourceDetails;
     }
    
     @Bean
     public OAuth2RestTemplate oAuth2RestTemplate() {
         return new OAuth2RestTemplate(client(), oauth2ClientContext());
     }
    
  4. Use the above OAuth2RestTemplate in the service class. In this way, the template will automatically generate the accessToken (if not already present)

     @Autowired
     private OAuth2RestTemplate oAuth2RestTemplate;
    
     public UIResponse findCustomerDetailsByNumberUsingOAuth(int customerNumber) {
         ...
         ResponseEntity<Customer> customerResponseEntity = oAuth2RestTemplate.getForEntity(
                 URL_CUSTOMER_SERVICE, Customer.class, customerNumber);
         ...
         ResponseEntity<List<Account>> accountResponseEntity = oAuth2RestTemplate.exchange(
                 URL_ACCOUNT_SERVICE, HttpMethod.GET, null,
                 new ParameterizedTypeReference<List<Account>>() {}, customerNumber);
         ...
     }
    
Written on May 9, 2018