Spring Boot Client App Tutorial
The purpose of the sample project is to show you how to write an OAuth2 client application for FusionCreator, with a Java framework - Spring Boot.
You will implement both the standard OAuth2 Authorization Code grant flow and the private key authentication based on asymmetric cryptography.
Get it from GitHub
For your convenience, this sample client application is available on GitHub, in the following repository: https://github.com/FusionFabric/ffdc-sample-springboot.
Clone it and follow the instructions from ffdc-authorization-code/README.md.
The provided GitHub repository contains also a sample client application - ffdc-client-credentials - that demonstrates the implementation of the OAuth2 Client Credentials grant flow which is not covered in the current tutorial.
Prerequisites
To build this client app you need a recent Java installation on your machine.
You must also create an application on FusionCreator that includes the Static Data for Trade Capture API. Use the following Reply URL:
http://localhost:8081/login/oauth2/code/finastra
Bootstrap App
In this section, you will use a web service called Spring Initializr to prepare a Spring Boot project.
To bootstrap your application
- Go to https://start.spring.io/ to use Spring Initializr.
- Enter the Project Metadata, as follows:
- Group: the group ID, such as
com.finastra
- Artifact: the artifact ID, which is your application name, such as:
productapp
- Group: the group ID, such as
- Search for and add the following dependencies:
- Thymleaf
- Spring Web
- Spring Boot DevTools
- Click Generate. A ZIP archive, with the name of your artifact, is ready for you to download.
- Unpack the downloaded archive, and open the project directory.
- Open pom.xml and add the following dependencies, apart from those already added by Spring Initializr:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.9</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.13</version>
</dependency>
...</dependencies>
Open a terminal, or a command prompt and run the Maven install command as follows:
- if you have Maven installed on your computer:
mvn install
- if you don’t have Maven:
mvnw install
The required dependencies are downloaded and added to your project.
Import the project in your favorite IDE.
You are now ready to code your application.
API Data Model
You start by implementing a data model that you will use to store and manipulate the data retrieved from the registered APIs.
To implement the data model
- Create a package named model.
- In model, create a class named TradeCaptureStaticData with the following members:
package com.finastra.productapp.model;
public class TradeCaptureStaticData {
private String id;
private String description;
private String [] applicableEntities;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String[] getApplicableEntities() {
return applicableEntities;
}
public void setApplicableEntities(String[] applicableEntities) {
this.applicableEntities = applicableEntities;
}
}
- Create a second class, named TradeCaptureStaticDataList with the following members:
package com.finastra.productapp.model;
public class TradeCaptureStaticDataList {
private TradeCaptureStaticData [] items;
private String description;
public TradeCaptureStaticData[] getItems() {
return items;
}
public void setItems(TradeCaptureStaticData[] items) {
this.items = items;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
Your data model contains a data class - TradeCaptureStaticData - to store a trade reference source and a list of reference sources - TradeCaptureStaticDataList - implemented as an array.
API Client
In this section you will create a class to retrieve the data from FusionFabric.cloud registered APIs. This API client class stores the data in dedicated data structures, such as a list of reference sources applicable to legal entities.
To code the FusionFabric.cloud API client
- Create a package named api.
- In api, create a new class named FfdcApi.
- In FfdcApi, add the following property, to fetch the base URL form the configuration file:
package com.finastra.productapp.api;
@Component
public class FfdcApi {
@Value("${ffdcapi.baseUrl}")
private String baseUrl;
}
- Add another member responsible for firing the HTTP connection:
@Component
public class FfdcApi {
//...
@Autowired
private RestTemplate restTemplate;
}
- Add a method to retrieve some reference sources from the API’s endpoint, and store it to a dedicated list -
TradeCaptureStaticDataList
:
@Component
public class FfdcApi {
//...
public TradeCaptureStaticDataList getReferenceSourcesLegalEntities () {
= UriComponentsBuilder
UriComponents uriBuilder .fromUriString(baseUrl + "/capital-market/trade-capture/static-data/v1/reference-sources")
.queryParam("applicableEntities", "legal-entities")
.build();
return restTemplate.exchange(uriBuilder.toUri(), HttpMethod.GET, null, TradeCaptureStaticDataList.class).getBody();
}
}
- Accept the import of the following packages:
import com.finastra.productapp.model.TradeCaptureStaticDataList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
Notes
- This method returns the list of reference sources that you will receive from the call.
- The query parameters are hard-coded so this method will work for reference sources as legal entities.
Security Configuration
In this section you will implement the configuration for Spring Security. You will do that by writing a configuration class - FfdcConfig
- that extends WebSecurityConfigurerAdapter
to meet the custom requirements of FusionFabric.cloud APIs.
To create the security configuration class
- Create a package, named config.
- In config, create a class, named FfdcConfig.
- Add the following members, that are provided by the application configuration file - application.yml:
package com.finastra.productapp.config;
@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class FfdcConfig extends WebSecurityConfigurerAdapter {
@Autowired
private OAuth2ClientContext oauth2ClientContext;
@Autowired
private OAuth2ClientContextFilter oauth2ClientContextFilter;
@Value("${ffdcapi.loginUrl}")
private String aud;
@Value("${oauth2.callbackPath}")
private String oauth2CallbackPath;
@Value("${oauth2.jwkSetUri}")
private String jwkSetUri;
}
- Set the OAuth2 grant type:
@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class FfdcConfig extends WebSecurityConfigurerAdapter {
//...
@Bean
@ConfigurationProperties("finastra.oauth2.client")
public AuthorizationCodeResourceDetails oAuthDetails() {
return new AuthorizationCodeResourceDetails();
}
}
- Implement the REST template for the default authorization code grant authentication - authentication with client secrets:
@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class FfdcConfig extends WebSecurityConfigurerAdapter {
//...
@Bean
public OAuth2RestTemplate restTemplate() {
return clientSecretRestTemplate();
}
private OAuth2RestTemplate clientSecretRestTemplate(){
= new OAuth2RestTemplate(oAuthDetails(), oauth2ClientContext);
OAuth2RestTemplate restTemplate = new AuthorizationCodeAccessTokenProvider ();
AuthorizationCodeAccessTokenProvider accessTokenProvider .setAuthenticationHandler(new DefaultClientAuthenticationHandler());
accessTokenProvider.setAccessTokenProvider(accessTokenProvider);
restTemplate= HttpClientBuilder.create().useSystemProperties().build();
CloseableHttpClient httpClient .setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient));
restTemplate.setRetryBadAccessTokens(false);
restTemplatereturn restTemplate;
}
}
- Configure the token services:
@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class FfdcConfig extends WebSecurityConfigurerAdapter {
//...
@Bean
public DefaultTokenServices tokenService (){
= new DefaultTokenServices();
DefaultTokenServices services .setTokenStore(tokenStore());
servicesreturn services;
}
}
- Configure the filter to acquire the access token from the FusionCreator Authorization server, based on the REST template defined in the previous step:
@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class FfdcConfig extends WebSecurityConfigurerAdapter {
//...
@Bean
public OAuth2ClientAuthenticationProcessingFilter oauth2ClientAuthenticationProcessingFilter() {
=
OAuth2ClientAuthenticationProcessingFilter filter new OAuth2ClientAuthenticationProcessingFilter(oauth2CallbackPath);
.setRestTemplate(restTemplate());
filter.setTokenServices(tokenService());
filterreturn filter;
}
}
- Set the authentication entry point:
@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class FfdcConfig extends WebSecurityConfigurerAdapter {
//...
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new LoginUrlAuthenticationEntryPoint(oauth2CallbackPath);
}
}
- Set the token store to the
jwkSetUri
. The value is configured in application.yml and provided by the Discovery service:
@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class FfdcConfig extends WebSecurityConfigurerAdapter {
//...
@Bean
public JwkTokenStore tokenStore() {
return new JwkTokenStore(jwkSetUri);
}
- Implement the
configure()
method, required by theWebSecurityConfigurerAdapter
parent class, to tailor the security implementation to your scenario:
@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class FfdcConfig extends WebSecurityConfigurerAdapter {
//...
@Override
protected void configure(HttpSecurity http) throws Exception {
.exceptionHandling()
http.authenticationEntryPoint(authenticationEntryPoint())
.and()
.authorizeRequests()
.antMatchers("/results").authenticated()
.anyRequest().permitAll()
.and()
.addFilterAfter(oauth2ClientContextFilter, ExceptionTranslationFilter.class)
.addFilterBefore(oauth2ClientAuthenticationProcessingFilter(), FilterSecurityInterceptor.class)
.logout()
.logoutUrl("/appLogout")
.logoutSuccessUrl("/logout");
}
}
- Accept all package imports suggested by your IDE:
import com.finastra.productapp.api.JwtClientAuthenticationHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter;
import org.springframework.security.oauth2.client.token.auth.DefaultClientAuthenticationHandler;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStore;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
Controller Class - App’s Endpoints
In this section you will create the classes responsible for mapping your client app endpoints to your app UI model.
You define two controller classes - a ProductController
and an ErrorController
To create the controller classes
- Create a package named endpoint.
- In endpoint, create a class named ProductController and add the following class definition:
package com.finastra.productapp.endpoint;
@Controller
class ProductController {
@Autowired
private FfdcApi ffdcApi;
}
- Implement the
/results
, home -/
, andlogout
endpoints:
@Controller
class ProductController {
// ....
@RequestMapping("/results")
public String resultsPage (Model model){
.addAttribute("entities" ,ffdcApi.getReferenceSourcesLegalEntities().getItems());
modelreturn "results";
}
@RequestMapping("/")
public String indexPage (Model model){
return "index";
}
@RequestMapping("/logout")
public String logout(Model model) {
return "logout";
}
}
- Import the required packages:
import com.finastra.productapp.api.FfdcApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
- In endpoint, create a class named ErrorController and add the following class definition:
package com.finastra.productapp.endpoint;
@ControllerAdvice
public class ErrorController {
private static final Logger log = LoggerFactory.getLogger(ErrorController.class);
@ExceptionHandler(Throwable.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String exception(final Throwable throwable, final Model model) {
.error("Exception during execution of SpringSecurity application", throwable);
logString errorMessage = (throwable != null ? throwable.getMessage() : "Unknown error");
.addAttribute("error", errorMessage);
modelreturn "error";
}
}
- Import the required packages:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
HTML Templates
Now that your model and controllers are ready, you need the view. It is based on HTML templates that you will implement in this section. The templating system is provided by Thymeleaf.
To enable the view templates
- In src.main/resources/templates, create the following subdirectories and empty text files:
- templates/
- layout/
- footer.html
- head.html
- header.html
- error.html
- index.html
- layout.html
- logout.html
- results.html
- layout/
- Add the following content to error.html:
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout" layout:decorate="layout">
<main class="main" layout:fragment="content">
<div class="row">
<div class="col-sm-12">
<div class="alert alert-danger" role="alert" th:text="${error}">
</div>
</div>
</div>
</main>
</html>
- Add the following content to index.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout" layout:decorate="layout">
<body class="container-fluid">
</body>
</html>
- Add the following content to layout.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout">
<head th:replace="layout/head :: head"></head>
<body class="container-fluid" >
<header th:replace="layout/header :: header"></header>
<main layout:fragment="content"></main>
<footer th:replace="layout/footer :: footer"></footer>
</body>
</html>
- Add the following content to logout.html:
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout" layout:decorate="layout">
<main class="main" layout:fragment="content">
<div class="row">
<div class="col-sm-12">
<div class="alert alert-success" role="alert">
You successfully removed the access token.</div>
</div>
</div>
</main>
</html>
- Add the following content to results.html:
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout" layout:decorate="layout">
<main class="main" layout:fragment="content">
<div class="row" >
<div class="col-lg-2 col-md-4 col-sm-6" th:each="entity: ${entities}">
<div class="card">
<div class="card-body">
<h5 class="card-title" th:text="${entity.id}"></h5>
<h6 class="card-subtitle mb-2 text-muted" th:text="${entity.description}"></h6>
<p></p>
<p class="card-text">
<strong>Applicable entities:</strong>
</p>
<ul class="list-group list-group-flush" th:each="detail: ${entity.applicableEntities}">
<li class="list-group-item" th:text="${detail}"></li>
</ul>
</div>
</div>
</div>
</div>
</main>
</html>
- Add the following content to layout/footer.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<footer th:fragment="footer">
<p class="text-center text-muted">© 2019 Finastra. All rights reserved.</p>
</footer>
</html>
- Add the following content to layout/head.html:
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head">
<title>Finastra Spring Boot Sample</title>
<!-- CSS (load bootstrap from a CDN) -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" th:href="@{/css/style.css}" >
</head>
</html>
- Add the following content to layout/header.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<header th::fragment="header">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<span class="navbar-brand mb-0 h1">Finastra Spring Boot Authorization Code Sample</span>
</nav>
<h6>
<nav class="navbar navbar-expand-lg navbar-light">
<span th:text="Secret Key Authentication"></span>
</nav>
</h6>
<div class="btn-toolbar" role="toolbar">
<form action="/results">
<button type="submit" class="btn btn-primary">Get Data</button>
</form>
<form th:action="@{/appLogout}" method="POST">
<button type="submit" class="btn btn-danger">Remove Access Token</button>
</form>
</div>
<hr>
</header>
</html>
- In src/main/resources/static create a directory named css, with an empty CSS file - style.css:
- static/
- css/
- style.css
- css/
- Add the following code to style.css:
.main {
height: calc(100% - 150px);
}
.navbar {
margin-bottom: 10px;
}
.btn {
margin: 10px;
}
body {
margin: 10px; }
App Configuration
You store the application configuration parameters in a YAML file.
To create the configuration file
- In /src/main/resources create an empty file, named application.yml.
- Add the following content:
server:
port: 8081
logging:
level:
org.springframework.web: INFO
org.springframework.security: INFO
com.finastra: DEBUG
ffdcapi:
baseUrl: https://api.fusionfabric.cloud
loginUrl: ${ffdcapi.baseUrl}/login/v1
finastra:
oauth2:
client:
clientId: <%YOUR-CLIENT-ID%>
clientSecret: <%YOUR-SECRET-KEY%>
accessTokenUri: ${ffdcapi.loginUrl}/sandbox/oidc/token
userAuthorizationUri: ${ffdcapi.loginUrl}/sandbox/oidc/authorize
scope: openid
oauth2.callbackPath: /login/oauth2/code/finastra
oauth2.jwkSetUri: ${ffdcapi.loginUrl}/oidc/jwks.json
- Replace
<%YOUR-CLIENT-ID%>
and<%YOUR-SECRET-KEY%>
with the actual client ID and secret key of the application that you registered on FusionCreator.
The userAuthorizationUri
, accessTokenUri
, oauth2.jwkSetUri
URLs are provided by the Discovery service.
Your application is configured to run on the port 8081.
Run your App
Your are now ready to run your client app.
To run your app
- If you have Maven installed on your machine, start your app with:
$ mvn spring-boot:run
Otherwise, with:
$ mvnw spring-boot:run
Point your browser to localhost:8081.
Click Get Data. You are redirected to the authentication page of FusionFabric.cloud Authorization Server.
Use one of the following pair of credentials, and then click Log In.
User | Password |
---|---|
ffdcuser1 |
123456 |
ffdcuser2 |
123456 |
If successful, you are redirected to your the results page of your application, and list of the reference sources retrieved from the FusionFabric.cloud Static Data for Trade Capture API is displayed.
- (Optional) Click Remove Access Token to clear the access token from your browser’ session.
Private Key Authentication
Private key authentication is an enhanced authentication method, based on asymmetric cryptography, where you use a pair of private and public RSA keys to sign a JSON Web Token (JWT) - with the private key, that you sent over the network to request the access token. The JWT is decoded with the public key that you submit to the Authorization Server in advance.
To enable private key authentication in your client application
Enable private key authentication in your application, as described in Private Key Authentication and store the private RSA key in a file named private.key.
Convert private.key to pkcs8 format. You can do that with OpenSSL, or other tool of choice. With OpenSSL, the command is:
openssl pkcs8 -topk8 -in private.key -nocrypt -out pkcs8_private.pem
Store pkcs8_private.pem in src/main/resources.
- Update src/main/resources/application.yml and add the following parameter:
auth:
keyId: <%keyID%>
strong: true
Replace
%keyID%
with the ID - kid, of the JWK file that you uploaded to your application on FusionCreator.
- In api, create a class named JwtClientAuthenticationHandler to handle the JWT generation and authentication:
package com.finastra.productapp.api;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpHeaders;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
import org.springframework.security.jwt.crypto.sign.Signer;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.auth.ClientAuthenticationHandler;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import java.security.PrivateKey;
import java.security.interfaces.RSAPrivateKey;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class JwtClientAuthenticationHandler implements ClientAuthenticationHandler, InitializingBean {
private static final Logger log = LoggerFactory.getLogger(JwtClientAuthenticationHandler.class);
public static final String CLIENT_ASSERTION_TYPE = "client_assertion_type";
public static final String CLIENT_ASSERTION = "client_assertion";
public static final String CLIENT_ASSERTION_TYPE_JWT = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
public static final int DEFAULT_EXPIRATION = 60 * 60;
private Signer signer;
private String keyId;
private String aud;
private int expiration = DEFAULT_EXPIRATION;
private ObjectMapper objectMapper = new ObjectMapper();
public JwtClientAuthenticationHandler() {}
@Override
public void authenticateTokenRequest(OAuth2ProtectedResourceDetails resource, MultiValueMap<String, String> form, HttpHeaders headers) {
if (resource.isAuthenticationRequired()) {
= newGrant(resource);
JwtGrant grant = null;
Jwt jwt try {
Map<String, String> jwtEncodeHeader = new HashMap<>(1);
.put("kid", keyId);
jwtEncodeHeader= JwtHelper.encode(objectMapper.writeValueAsString(grant), signer, jwtEncodeHeader);
jwt .set(CLIENT_ASSERTION_TYPE, CLIENT_ASSERTION_TYPE_JWT);
form.set(CLIENT_ASSERTION, jwt.getEncoded());
form} catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
}
}
private JwtGrant newGrant(OAuth2ProtectedResourceDetails resource) {
int currentTimeSeconds = (int) (System.currentTimeMillis() / 1000);
= new JwtGrant();
JwtGrant jwtGrant .setJti(UUID.randomUUID().toString());
jwtGrant.setIssuer(resource.getClientId());
jwtGrant.setSubject(resource.getClientId());
jwtGrant.setAudience(aud);
jwtGrant.setExpires(currentTimeSeconds + expiration);
jwtGrant.setIssuedAt(currentTimeSeconds);
jwtGrantreturn jwtGrant;
}
public void setKeyId(String keyId) {
this.keyId = keyId;
}
public void setExpiration(int expiration) {
this.expiration = expiration;
}
public void setAud(String aud) {
this.aud = aud;
}
@Override
public void afterPropertiesSet() {
if (keyId == null) {
throw new IllegalArgumentException("keystore property is required");
}
try {
PrivateKey privateKey = PrivateKeyReader.get("pkcs8_private.pem");
.state(privateKey instanceof RSAPrivateKey, "KeyPair must be an RSA");
Assert= new RsaSigner((RSAPrivateKey) privateKey);
signer } catch (Exception e) {
.error("failed to generate private key", e);
log}
}
private static class JwtGrant {
@JsonProperty("jti")
private String jti;
@JsonProperty("iss")
private String issuer;
@JsonProperty("sub")
private String subject;
@JsonProperty("aud")
private String audience;
@JsonProperty("exp")
private int expires;
@JsonProperty("iat")
private int issuedAt;
public String getJti() { return jti;}
public void setJti(String jti) { this.jti = jti; }
public String getIssuer() {
return issuer;
}
public void setIssuer(String issuer) {
this.issuer = issuer;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getAudience() {
return audience;
}
public void setAudience(String audience) {
this.audience = audience;
}
public int getExpires() {
return expires;
}
public void setExpires(int expires) {
this.expires = expires;
}
public int getIssuedAt() {
return issuedAt;
}
public void setIssuedAt(int issuedAt) {
this.issuedAt = issuedAt;
}
}
}
- In api, create a helper class, named PrivateKeyReader to read the private key that you use to sign the JWT:
package com.finastra.productapp.api;
import org.apache.commons.codec.binary.Base64;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
public class PrivateKeyReader {
public static PrivateKey get(String resourceName) throws Exception {
String privateKeyPEM = new String(Files.readAllBytes(Paths.get(ClassLoader.getSystemResource(resourceName).toURI())), StandardCharsets.UTF_8);
= privateKeyPEM
privateKeyPEM .replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "");
byte[] privateKeyDER = Base64.decodeBase64(privateKeyPEM );
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyDER);
return KeyFactory.getInstance("RSA").generatePrivate(spec);
}
}
- Add the following members to the FfdcConfig class. If some already exist, update them.
public class FfdcConfig extends WebSecurityConfigurerAdapter {
//...
@Value("${auth.strong}")
private boolean authStrong;
@Value("${auth.keyId}")
private String keyId;
@Bean
public OAuth2RestTemplate restTemplate() {
return authStrong ? clientJwtRestTemplate() : clientSecretRestTemplate();
}
private OAuth2RestTemplate clientJwtRestTemplate() {
= new OAuth2RestTemplate(oAuthDetails(),oauth2ClientContext);
OAuth2RestTemplate restTemplate = new AuthorizationCodeAccessTokenProvider ();
AuthorizationCodeAccessTokenProvider accessTokenProvider .setAuthenticationHandler(jwtClientAuthenticationHandler());
accessTokenProvider.setAccessTokenProvider(accessTokenProvider);
restTemplate= HttpClientBuilder.create().useSystemProperties().build();
CloseableHttpClient httpClient .setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient));
restTemplate.setRetryBadAccessTokens(false);
restTemplatereturn restTemplate;
}
private JwtClientAuthenticationHandler jwtClientAuthenticationHandler() {
= new JwtClientAuthenticationHandler();
JwtClientAuthenticationHandler authenticationHandler .setKeyId(keyId);
authenticationHandler.setAud(aud);
authenticationHandler.afterPropertiesSet();
authenticationHandlerreturn authenticationHandler;
}
@Bean
public JwkTokenStore tokenStore() {
return new JwkTokenStore(jwkSetUri);
}
}
Include the required package import:
import com.finastra.productapp.api.JwtClientAuthenticationHandler;
- Update the ProductController class by adding the
strongAuth
attribute to the model:
@Controller
class ProductController {
// ....
@Value("${auth.strong}")
private boolean strongAuth;
@RequestMapping("/results")
public String resultsPage (Model model){
.addAttribute("entities" ,ffdcApi.getReferenceSourcesLegalEntities().getItems());
model.addAttribute("strongAuth", strongAuth);
modelreturn "results";
}
@RequestMapping("/")
public String indexPage (Model model){
.addAttribute("strongAuth", strongAuth);
modelreturn "index";
}
@RequestMapping("/logout")
public String logout(Model model) {
.addAttribute("strongAuth", strongAuth);
modelreturn "logout";
}
- Update the UI template file layout/header.html as follows to adapt the heading to the authentication type you are using:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<header th::fragment="header">
<h6>
<nav class="navbar navbar-expand-lg navbar-light">
<span th:text="${strongAuth} ? 'JSON Web Key Authentication' : 'Secret Key Authentication' "></span>
</nav>
</h6>
</header>
</html>
- Run your application as described in the previous section.
Final Code Review
Here are the code files discussed on this page.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.9</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.13</version>
</dependency>
...
</dependencies>
package com.finastra.productapp.model;
public class TradeCaptureStaticDataList {
private TradeCaptureStaticData [] items;
private String description;
public TradeCaptureStaticData[] getItems() {
return items;
}
public void setItems(TradeCaptureStaticData[] items) {
this.items = items;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
package com.finastra.productapp.api;
@Componentpublic class FfdcApi {
Value("${ffdcapi.baseUrl}")
@private String baseUrl;
@Autowiredprivate RestTemplate restTemplate;
public TradeCaptureStaticDataList getReferenceSourcesLegalEntities () {
= UriComponentsBuilder
UriComponents uriBuilder .fromUriString(baseUrl + "/capital-market/trade-capture/static-data/v1/reference-sources")
.queryParam("applicableEntities", "legal-entities")
.build();
return restTemplate.exchange(uriBuilder.toUri(), HttpMethod.GET, null, TradeCaptureStaticDataList.class).getBody();
}
}
import com.finastra.productapp.model.TradeCaptureStaticDataList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder
package com.finastra.productapp.config;
@Configuration
@EnableWebSecurity
@EnableOAuth2Clientpublic class FfdcConfig extends WebSecurityConfigurerAdapter {
@Autowiredprivate OAuth2ClientContext oauth2ClientContext;
@Autowiredprivate OAuth2ClientContextFilter oauth2ClientContextFilter;
Value("${ffdcapi.loginUrl}")
@private String aud;
Value("${oauth2.callbackPath}")
@private String oauth2CallbackPath;
Value("${oauth2.jwkSetUri}")
@private String jwkSetUri;
ConfigurationProperties("finastra.oauth2.client")
@public AuthorizationCodeResourceDetails oAuthDetails() {
return new AuthorizationCodeResourceDetails();
}
}
@Beanpublic OAuth2RestTemplate restTemplate() {
return clientSecretRestTemplate();
}
private OAuth2RestTemplate clientSecretRestTemplate(){
= new OAuth2RestTemplate(oAuthDetails(), oauth2ClientContext);
OAuth2RestTemplate restTemplate = new AuthorizationCodeAccessTokenProvider ();
AuthorizationCodeAccessTokenProvider accessTokenProvider .setAuthenticationHandler(new DefaultClientAuthenticationHandler());
accessTokenProvider.setAccessTokenProvider(accessTokenProvider);
restTemplate= HttpClientBuilder.create().useSystemProperties().build();
CloseableHttpClient httpClient .setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient));
restTemplate.setRetryBadAccessTokens(false);
restTemplatereturn restTemplate;
}
public DefaultTokenServices tokenService (){
= new DefaultTokenServices();
DefaultTokenServices services .setTokenStore(tokenStore());
servicesreturn services;
}
public OAuth2ClientAuthenticationProcessingFilter oauth2ClientAuthenticationProcessingFilter() {
=
OAuth2ClientAuthenticationProcessingFilter filter new OAuth2ClientAuthenticationProcessingFilter(oauth2CallbackPath);
.setRestTemplate(restTemplate());
filter.setTokenServices(tokenService());
filterreturn filter;
}
public AuthenticationEntryPoint authenticationEntryPoint() {
return new LoginUrlAuthenticationEntryPoint(oauth2CallbackPath);
}
public JwkTokenStore tokenStore() {
return new JwkTokenStore(jwkSetUri);
}
@Overrideprotected void configure(HttpSecurity http) throws Exception {
.exceptionHandling()
http.authenticationEntryPoint(authenticationEntryPoint())
.and()
.authorizeRequests()
.antMatchers("/results").authenticated()
.anyRequest().permitAll()
.and()
.addFilterAfter(oauth2ClientContextFilter, ExceptionTranslationFilter.class)
.addFilterBefore(oauth2ClientAuthenticationProcessingFilter(), FilterSecurityInterceptor.class)
.logout()
.logoutUrl("/appLogout")
}
import com.finastra.productapp.api.JwtClientAuthenticationHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter;
import org.springframework.security.oauth2.client.token.auth.DefaultClientAuthenticationHandler;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStore;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
package com.finastra.productapp.endpoint;
@Controllerclass ProductController {
@Autowiredprivate FfdcApi ffdcApi;
RequestMapping("/results")
@public String resultsPage (Model model){
.addAttribute("entities" ,ffdcApi.getReferenceSourcesLegalEntities().getItems());
modelreturn "results";
}
RequestMapping("/")
@public String indexPage (Model model){
return "index";
}
RequestMapping("/logout")
@public String logout(Model model) {
return "logout";
}
}
import com.finastra.productapp.api.FfdcApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
package com.finastra.productapp.endpoint;
@ControllerAdvicepublic class ErrorController {
private static final Logger log = LoggerFactory.getLogger(ErrorController.class);
ExceptionHandler(Throwable.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@public String exception(final Throwable throwable, final Model model) {
.error("Exception during execution of SpringSecurity application", throwable);
logString errorMessage = (throwable != null ? throwable.getMessage() : "Unknown error");
.addAttribute("error", errorMessage);
modelreturn "error";
}
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout" layout:decorate="layout">
<main class="main" layout:fragment="content">
<div class="row">
<div class="col-sm-12">
<div class="alert alert-danger" role="alert" th:text="${error}">
</div>
</div>
</div>
</main>
</html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout" layout:decorate="layout">
<main class="main" layout:fragment="content">
<div class="row">
<div class="col-sm-12">
<div class="alert alert-danger" role="alert" th:text="${error}">
</div>
</div>
</div>
</main>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout" layout:decorate="layout">
<body class="container-fluid">
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout">
<head th:replace="layout/head :: head"></head>
<body class="container-fluid" >
<header th:replace="layout/header :: header"></header>
<main layout:fragment="content"></main>
<footer th:replace="layout/footer :: footer"></footer>
</body>
</html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout" layout:decorate="layout">
<main class="main" layout:fragment="content">
<div class="row">
<div class="col-sm-12">
<div class="alert alert-success" role="alert">
.
You successfully removed the access token</div>
</div>
</div>
</main>
</html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout" layout:decorate="layout">
<main class="main" layout:fragment="content">
<div class="row" >
<div class="col-lg-2 col-md-4 col-sm-6" th:each="entity: ${entities}">
<div class="card">
<div class="card-body">
<h5 class="card-title" th:text="${entity.id}"></h5>
<h6 class="card-subtitle mb-2 text-muted" th:text="${entity.description}"></h6>
<p></p>
<p class="card-text">
<strong>Applicable entities:</strong>
</p>
<ul class="list-group list-group-flush" th:each="detail: ${entity.applicableEntities}">
<li class="list-group-item" th:text="${detail}"></li>
</ul>
</div>
</div>
</div>
</div>
</main>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<footer th:fragment="footer">
<p class="text-center text-muted">© 2019 Finastra. All rights reserved.</p>
</footer>
</html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head">
<title>Finastra Spring Boot Sample</title>
<!-- CSS (load bootstrap from a CDN) -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
integrity<link rel="stylesheet" th:href="@{/css/style.css}" >
</head>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<header th::fragment="header">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<span class="navbar-brand mb-0 h1">Finastra Spring Boot Authorization Code Sample</span>
</nav>
<h6>
<nav class="navbar navbar-expand-lg navbar-light">
<span th:text="Secret Key Authentication"></span>
</nav>
</h6>
<div class="btn-toolbar" role="toolbar">
<form action="/results">
<button type="submit" class="btn btn-primary">Get Data</button>
</form>
<form th:action="@{/appLogout}" method="POST">
<button type="submit" class="btn btn-danger">Remove Access Token</button>
</form>
</div>
<hr>
</header>
</html>
.main {
height: calc(100% - 150px);
}
.navbar {
-bottom: 10px;
margin
}
.btn {
margin: 10px;
}
body {margin: 10px;
}</html>
:
server: 8081
port
:
logging:
level.springframework.web: INFO
org.springframework.security: INFO
org.finastra: DEBUG
com
:
ffdcapi: https://api.fusionfabric.cloud
baseUrl: ${ffdcapi.baseUrl}/login/v1
loginUrl
:
finastra:
oauth2:
client: <%YOUR-CLIENT-ID%>
clientId: <%YOUR-SECRET-KEY%>
clientSecret: ${ffdcapi.loginUrl}/sandbox/oidc/token
accessTokenUri userAuthorizationUri: ${ffdcapi.loginUrl}/sandbox/oidc/authorize
: openid
scope
.callbackPath: /login/oauth2/code/finastra
oauth2.jwkSetUri: ${ffdcapi.loginUrl}/oidc/jwks.json
oauth2