1 Holding Identity short Hash
djmil edited this page 2024-11-24 18:21:06 +01:00

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");
        }
    }

}