HoldingIdentityHash
parent
f576b4cd7b
commit
146ab460aa
262
Holding Identity short Hash.md
Normal file
262
Holding Identity short Hash.md
Normal file
@ -0,0 +1,262 @@
|
||||
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
|
||||
|
||||
```mermaid
|
||||
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](https://spring.io/guides/topicals/spring-security-architecture/) 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:
|
||||
|
||||
```java
|
||||
@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`*
|
||||
```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.
|
||||
|
||||
```java
|
||||
@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.
|
||||
|
||||
```java
|
||||
@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`*
|
||||
```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:
|
||||
|
||||
```java
|
||||
@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`*
|
||||
```java
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
22
Home.md
22
Home.md
@ -1,24 +1,27 @@
|
||||
|
||||
|
||||
# High Level Overview
|
||||
|
||||
This project is focused on exploring Corda Application that will be managing rules of the Checkers game.
|
||||
|
||||
It was decided to keep out of scope all Corda Cluster management tasks. As result, SpringBoot server *(which acts as a intermediary gateway between the world and a CordApp)* will be using solely default `admin` access rights.
|
||||
It was decided to keep out of scope all Corda Cluster management tasks. As result, SpringBoot server *(which acts as a intermediary gateway between the world and a CordApp)* will be using solely default `admin` access rights.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
ReactJS <--> SpringBoot -- admin ---> C[(Corda)]
|
||||
ReactJS <--> SpringBoot
|
||||
subgraph midleware
|
||||
direction LR
|
||||
SpringBoot -.users.-U[(Hardcoded DB)]
|
||||
end
|
||||
SpringBoot -- admin ---> C[(Corda)]
|
||||
C --- c1([Alice])
|
||||
C --- c2([Bob])
|
||||
C --- c3{Notary}
|
||||
C --- c4([Dave])
|
||||
C --- c4([Kumar])
|
||||
C --- c5([Charlie])
|
||||
```
|
||||
|
||||
Although it is possible to add MDM instance to the game, and enforce `users`, `groups`, `roles` and `permissions`. At the moment, the setup will be using set of default users, provided by CSDE.
|
||||
|
||||
On a front end side, before initiating any interactions, a new user has to first registered, with the name identical to one of a `VirtualNodes` names from Corda Cluster. That is how SpringBoot server will know on behalf of which identity a given operation shall be performed.
|
||||
On a front end side, before initiating any interactions, a login credentials has to be provided. The name and a password of a user has to match SpringBoot hardcoded users, which on itself is a representation of set of predefined Corda users, aka `VirtualNode` names from Corda Cluster. Such a scheme, ensures that SpringBoot server will always know on behalf of which identity a given operation shall be performed (despite the fact that in reality it will be executed under `admin` role).
|
||||
|
||||
# Major development milestones
|
||||
|
||||
@ -35,5 +38,12 @@ On a front end side, before initiating any interactions, a new user has to first
|
||||
- [[Secure REST Client]]
|
||||
Communication channel between SpringBoot server and CordApp.
|
||||
|
||||
- [[Holding Identity short Hash]]
|
||||
A CordaCheckers end user shall be able to perform actions (to play the game) on behalf of his representation in Corda Cluster. This means, that SpringBoot middleware shall be able to authenticate REST requests from React app and translate them onto request aimed to concrete virtual node: Alice, Bob, Kumar, Charlie, etc. One way of doing this, is to have hardcoded set of users. But this is *boring*. Instead, SpringBoot middleware is going to dynamically obtain information about available virtual nodes (their `names` and `short hashes`) and dynamically register set of relevant front end users.
|
||||
|
||||
- Sequence diagrams
|
||||
A high level overview of an execution pass for major actions.
|
||||
|
||||
|
||||
# TODOs
|
||||
- Use [jsonschema2pojo](https://github.com/joelittlejohn/jsonschema2pojo/tree/master/jsonschema2pojo-gradle-plugin) to generate POJO objects for SpringBoot server from REST API schemas provided by CSDE
|
@ -6,8 +6,7 @@ In SpringInitializer, create a simple SpringBoot RESTfull app to server bad joke
|
||||
```java
|
||||
package djmil.cordacheckers;
|
||||
|
||||
public record Joke(String joke) {
|
||||
}
|
||||
public record Joke(String joke) { }
|
||||
```
|
||||
|
||||
*`src/main/java/djmil/cordacheckers/ApiController.java`*
|
||||
|
25
Secure ReactJS App.md
Normal file
25
Secure ReactJS App.md
Normal file
@ -0,0 +1,25 @@
|
||||
# JWT vs Server-side Session
|
||||
|
||||
## Sessions
|
||||
|
||||
With server-side sessions, you will either have to store the session identifier in a database, or else keep it in memory and make sure that the client always hits the same server. Both of these have drawbacks. In the case of the database (or other centralised storage), this becomes a bottleneck and a thing to maintain - essentially an extra query to be done with every request.
|
||||
|
||||
With an in-memory solution, you limit your horizontal scaling, and sessions will be affected by network issues (clients roaming between Wifi and mobile data, servers rebooting, etc).
|
||||
|
||||
## JWT
|
||||
|
||||
Moving the session to the client means that you remove the dependency on a server-side session, but it imposes its own set of challenges.
|
||||
|
||||
- Storing the token securely.
|
||||
- Transporting it securely.
|
||||
- JWT sessions can sometimes be hard to invalidate.
|
||||
- Trusting the client's claim.
|
||||
|
||||
These issues are shared by JWTs and other client-side session mechanisms alike.
|
||||
|
||||
JWT, in particular, addresses the last of these. It may help to understand what a JWT is:
|
||||
|
||||
It is a bit of information. For user sessions, you could include the username and the time when the token expires. But it could conceivably be anything, even the session ID or the user's entire profile (please don't do that though). It has got a secure signature that prevents malicious parties from generating fake tokens (you need access to the server's private key to sign them and you can verify that they were not modified after they were signed). You send them with every request, just like a cookie or `Authorization` Header would be sent. In fact, they are commonly sent in the HTTP `Authorization` header but using a cookie is fine too.
|
||||
|
||||
https://www.rdegges.com/2018/please-stop-using-local-storage/
|
||||
|
27
Sequence diagrams.md
Normal file
27
Sequence diagrams.md
Normal file
@ -0,0 +1,27 @@
|
||||
|
||||
# Get Active Games list
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant JS as FrontEnd
|
||||
participant SB as SpringBoot
|
||||
participant C as Corda
|
||||
JS->>JS: Authenticate as Alice
|
||||
JS->>SB: Get Active Games
|
||||
activate SB
|
||||
SB->>SB: Generate Client RequestID [CiD]
|
||||
SB->>SB: Fetch HoldingIdentityHash [HiH] for Alice
|
||||
SB->>+C: POST: /flow/HiH<br>{flow: ListActiveGames,<br>ClientRequerstID: CiD}
|
||||
|
||||
C-->>C: StartFlow
|
||||
C-->>SB: FlowStatus=STARTED
|
||||
loop untill Status!=COMPLETED
|
||||
SB->>C: GET: flow/HiD/CiD
|
||||
C-->>C: CheckFlow
|
||||
C-->>-SB: Status,<br>List[ActiveGames]
|
||||
end
|
||||
|
||||
|
||||
SB->>SB: Pack: List[ActiveGames]
|
||||
SB-->>JS: Packed List[ActiveGames]
|
||||
deactivate SB
|
||||
```
|
Loading…
Reference in New Issue
Block a user