In short, we want to avoid having hardcoded set of available users. Instead, SpringBoot middleware is going to use Corda Cluster configuration as a source of truth in a task of managing frontend user logins.
Prerequisites
flowchart TD
subgraph "initilization (during startup)"
x500Name -->|used to create| VirtuaNode -->|on success assigned with| HoldingIdentity_shortHash
end
subgraph "execution (serving REST api)"
REST -->|HiH, flowName| VirtualNode -->|execute| Flow
end
UserDetails
Ideally, we want to link Holding Identity short Hash (HiH) with Principal information. This way, SpringSecurity would be responsible to dealing with user authentication, and only proper authorized users would be able to trigger corresponding Corda Flow:
@RestController
public class ApiController {
@Autowired
CordaClient cordaClient;
@GetMapping("/api/activegames")
public ResponseEntity<String> dashboard(@AuthenticationPrincipal ApiUserDetails user) {
System.out.println("Fetch active games list for user:"
+ user.getUsername());
List<Game> activeGames =
cordaClient.fetchActiveGamesList(user.getShortHash())
return ResponseEntity.ok(activeGames);
}
}
To create a Custom User Details class, we have to implement org.springframework.security.core.userdetails.UserDetails
interface. To save some time and efforts, we are going simply to extent implementation of org.springframework.security.core.userdetails.User
.
ApiUserDetails.java
package djmil.cordacheckers;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
public class ApiUserDetails extends User {
private final String shortHash;
public ApiUserDetails(UserDetails user, String shortHash) {
super(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
this.shortHash = shortHash;
}
public String getShortHash() {
return this.shortHash;
}
}
UserDetailService
Next, we have to provide a way of registering our custom users within SpringSecurity framework.
For this, an implementation of a UserDetailsService
interface has to provided.
@Service
public class ApiUserDetailsService implements UserDetailsService {
private final PasswordEncoder encoder;
private final ShortHashManager shortHashManager;
public ApiUserDetailsService(
PasswordEncoder encoder,
ShortHashManager shortHashManager) {
this.encoder = encoder;
this.shortHashManager = shortHashManager;
}
@Override
public ApiUserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
String shortHash = shortHashManager.getShortHashBy(username);
if (shortHash == null) {
throw new UsernameNotFoundException("ShortHash for user '"
+username+ "' not found");
}
System.out.println("Load user "+username);
User.UserBuilder userBuilder = User.builder();
UserDetails user = userBuilder
.username(username)
.password(encoder.encode("qaz123"))
.build();
return new ApiUserDetails(user, shortHash);
}
}
Classic implementation for loadUserByUsername()
is to fetch user details from some sort of permanent storage, like DB for example. In our case, we are trying to fetch a name of a provided user within a map of known Corda Identities.
[!Note] Hardcoded passwords Our implementation is going to force all users to use the same default hardcoded password. In reality, custom
ApiUserDetailsService
should use some sort of DB, to at least store a password per user. Also, the real production REST API shall force users to change their password on first successful login. But for this demo project, all important security question has been deliberately left out of scope.
Holding identity shorthash service
This is pretty naive implementation, which does the most strait forward job of getting list of available corda users with corresponding shortHash values. In reality, this service should be capable of not only obtaining its data during creation, but also to update it in a runtime, if users configuration within corda cluster was changed for some reason.
@Service
public class ShortHashManager {
static final Locale locale = Locale.getDefault();
Map<String, String> cnName2shortHash;
ShortHashManager(CordaClient client) {
this.cnName2shortHash = setCnName2shortHash(client);
}
private static Map<String, String> setCnName2shortHash(CordaClient client) {
Map<String, String> map = new HashMap<>();
List<virtualNodes> vNodesList = client.getVirtualnode();
try {
for (virtualNodes vNode : vNodesList) {
var identity = vNode.holdingIdentity();
if (identity.isPlayer()) {
map.put(identity.getName().toLowerCase(locale), identity.shortHash());
}
}
} catch (InvalidNameException e) {
// TODO: log
System.out.println("Unable to get ShorHash map for vNodes: "
+e.getExplanation());
}
System.out.println("ApiUserShortHashMap " + map);
return map;
}
String getShortHashBy(String apiUserName) {
return this.cnName2shortHash.get(apiUserName.toLowerCase(locale));
}
}
To get the required information, CordaClient
class is using /api/v1/virtualnode
REST endpoint from Corda cluster. Detail implementation of this link can be found in Secure REST Client page.
Security Config
Since we have already exposed our custom implementation of UserDetailService
under @Service
annotation, SpringBoot shall be able to find and use our customly made users for authentication purposes. In such case, SecurityConfig
for our application should look something like this:
SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
If we do not want to store user passwords in a plain from, we have to expose and use PasswordEncoder
. Thus we allow for DefaultAuthenticationProvoder
and ApiUserDetailsService
to syncup and understand each others way of password storing and validation.
Because shortHash vale, that we have added to the Principal
data, has nothing to do with actual user authentication - the default user authentication logic, provided by SpringSecurity, fits our need for a 100%. It will force user authentication for all exposed endpoints. Any REST client, albeit simple GET request via browser or fully fledged ReractApp - should provide legit user name and password in order for request to succeed.
But it is also possible to force SpringBoot to use our CustomAuthenticationProvider:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
public void configure(
AuthenticationManagerBuilder auth,
CustomAuthenticationProvider authenticationProvider)
throws Exception {
auth.authenticationProvider(authenticationProvider);
}
}
Essentially, here we are registering our custom authentication provider within SpringSecurity context.
Authentication Provider
Custom authentication provider shall implement AuthenticationProvider
interface. The whole point of this class, is to validate raw password from a incoming REST request with the one associated to our custom user (if a user with provided name is exists and known). Also, this code shall enforce roles for RBAC
and any other desirable authentication logic.
On success - custom authentication provider shall grant an AuthenticationToken
, which than can be reached by and used in Controller
code to perform any additional actions.
CustomAuthenticationProvider
package djmil.cordacheckers;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final PasswordEncoder encoder;
private final ApiUserDetailsService userDetailsService;
public CustomAuthenticationProvider(
PasswordEncoder encoder,
ApiUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
this.encoder = encoder;
}
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
ApiUserDetails user = userDetailsService.loadUserByUsername(username);
return checkPassword(user, password);
}
@Override
public boolean supports(Class<?> aClass) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass);
}
private Authentication checkPassword(
ApiUserDetails user,
String rawPassword) {
if (encoder.matches(rawPassword, user.getPassword())) {
return new UsernamePasswordAuthenticationToken(
user,
user.getPassword(),
user.getAuthorities());
} else {
throw new BadCredentialsException("Bad credentials");
}
}
}