Sunday, 5 July 2020

Spring Microservices with Kubernetes on Google, Ribbon, Feign and Spring Cloud Gateway

In the previous post, we built a JWT authentication server. Now we will build on that server and create an initial instance of the application that I am working on. I have been working on a education engine called Varahamihir. We will talk about this engine at a later date, but for the scope of this post, we will define this engine as below.

The engine consists of an authorization and routing service. We have taken our authorization server from the last post and added the spring cloud gateway routing functionality to that. This will be the only micro-service in our architecture that will be exposed to internet while all other micro-services will be strictly internal services.
The authorization services maintains all the users that exist in the system but these users are provided access to different resources based on their role. When designing micro-services based solutions, people have following two options.
  1. Authorization server performs necessary authentication and authorization and then for communication between micro-services specific client tokens are used.
  2. The authorization token received from the user is passed to each of the micro-services. This requires us to make sure the token is passed around for completing the life-cycle of the request.
I always prefer option 2 for better audit and tracing capabilities. Let's look all the things that we need to make this solution work.

Gateway Changes

As we have already mentioned, we are re-purposing our authorization server to also double up as routing gateway.
spring:
cloud:
gateway:
# default-filters:
# - name: SCGWGlobalFilter
routes:
- id: student
uri: http://student-service:8081/
predicates:
- Path=/*/registration/student,/*/student/**
filters:
- name: VarahamihirGatewayRequestPreFilter
- name: VarahamihirGatewayRequestPostFilter
- id: guardian
uri: http://guardian-service:8081/
predicates:
- Path=/*/registration/guardian,/*/guardian/**
filters:
- name: VarahamihirGatewayRequestPreFilter
- name: VarahamihirGatewayRequestPostFilter

As we can see in the yaml file, we define two routes, one for our student-service, and another for our guardian-service. Since we are going to use Kubernetes Service Discovery, these names have to match the service names declared in our pod configuration. Please be careful to only used exposed service names.
In the yaml file above, we see two filters defined. These are the hooks to add/remove something to all the requests that are being handled by Spring Cloud Gateway.
package com.avasthi.varahamihir.common.filters;
import lombok.extern.log4j.Log4j2;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
@Component
@Log4j2
public class VarahamihirGatewayRequestPreFilter extends AbstractGatewayFilterFactory<VarahamihirGatewayRequestPreFilter.Config> {
public VarahamihirGatewayRequestPreFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate().header("scgw-pre-header", Math.random()*10+"").build();
ServerHttpResponse response = exchange.getResponse();
request.getHeaders().forEach((k,v)->{
log.info(String.format("Request headers : %s: %s", k, v));
});
HttpHeaders headers = response.getHeaders();
headers.forEach((k,v)->{
log.info(String.format("Response headers : %s: %s", k, v));
});
return chain.filter(exchange.mutate().request(request).build());
};
}
public static class Config {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
}

package com.avasthi.varahamihir.common.filters;
import lombok.extern.log4j.Log4j2;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
@Log4j2
public class VarahamihirGatewayRequestPostFilter extends AbstractGatewayFilterFactory<VarahamihirGatewayRequestPostFilter.Config> {
public VarahamihirGatewayRequestPostFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
log.info("inside SCGWPostFilter.apply method...");
return(exchange, chain)->{
return chain.filter(exchange).then(Mono.fromRunnable(()->{
ServerHttpResponse response = exchange.getResponse();
exchange.getRequest().getHeaders().forEach((k,v)->{
log.info(String.format("Request headers : %s: %s", k, v));
});
HttpHeaders headers = response.getHeaders();
headers.forEach((k,v)->{
log.info(String.format("headers : %s: %s", k, v));
System.out.println(k + " : " + v);
});
}));
};
}
public static class Config {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
}


For now these filters are just printing the request headers and are not performing anything useful. We also need to make changes to our SpringApplication to enable discovery for the services.
package com.avasthi.varahamihir.identityserver;
import com.avasthi.varahamihir.common.constants.MyConstants;
import com.avasthi.varahamihir.common.constants.VarahamihirConstants;
import com.avasthi.varahamihir.common.enums.Role;
import com.avasthi.varahamihir.common.filters.VarahamihirGatewayRequestPostFilter;
import com.avasthi.varahamihir.common.filters.VarahamihirGatewayRequestPreFilter;
import com.avasthi.varahamihir.common.pojos.VarahamihirGrantedAuthority;
import com.avasthi.varahamihir.identityserver.entities.Tenant;
import com.avasthi.varahamihir.identityserver.entities.VarahamihirClientDetails;
import com.avasthi.varahamihir.identityserver.services.ClientService;
import com.avasthi.varahamihir.identityserver.services.TenantService;
import io.dekorate.kubernetes.annotation.ImagePullPolicy;
import io.dekorate.kubernetes.annotation.KubernetesApplication;
import io.dekorate.kubernetes.annotation.Probe;
import io.dekorate.kubernetes.annotation.ServiceType;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.reactive.config.EnableWebFlux;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static reactor.core.publisher.Hooks.onOperatorDebug;
/**
* Created by vinay on 1/8/16.
*/
@EnableWebFlux
@SpringBootApplication(scanBasePackages = {"com.avasthi.varahamihir"})
@Configuration
@EnableDiscoveryClient
@EntityScan(basePackages = {"com.avasthi.varahamihir"})
@EnableJpaRepositories("com.avasthi.varahamihir")
@KubernetesApplication(livenessProbe = @Probe(httpActionPath = "/manage/health"),
readinessProbe = @Probe(httpActionPath = "/manage/health"),
serviceType = ServiceType.NodePort,
imagePullPolicy = ImagePullPolicy.Always)
@PropertySource("classpath:jwt.properties")
@Log4j2
public class VarahamihirIdentityManagerLauncher {
@Autowired
private ClientService clientService;
@Autowired
private TenantService tenantService;
public static void main(String[] args) throws Exception {
SpringApplication.run(VarahamihirIdentityManagerLauncher.class, args);
}
@PostConstruct
public void initialize() {
onOperatorDebug();
Tenant tenant = new Tenant();
if (tenantService.count() == 0) {
tenant.setDescription("Default tenant for the system");
tenant.setDiscriminator(VarahamihirConstants.DEFAULT_TENANT);
tenant.setName("Default Tenant");
tenant.setDefaultValue(true);
tenant.setExpiry(VarahamihirConstants.DEFAULT_ACCESS_TOKEN_VALIDITY);
tenant.setRefreshExpiry(VarahamihirConstants.DEFAULT_REFRESH_TOKEN_VALIDITY);
tenant.setId(UUID.randomUUID());
tenant = tenantService.save(tenant);
if (clientService.count() == 0) {
VarahamihirClientDetails clientDetails = VarahamihirClientDetails.builder()
.id(UUID.randomUUID())
.clientId(VarahamihirConstants.DEFAULT_CLIENT)
.clientSecret(VarahamihirConstants.DEFAULT_SECRET)
.accessTokenValidity(VarahamihirConstants.DEFAULT_ACCESS_TOKEN_VALIDITY)
.refreshTokenValidity(VarahamihirConstants.DEFAULT_REFRESH_TOKEN_VALIDITY)
.authorities(Arrays.asList(Role.ADMIN.name(), Role.GUARDIAN.name(), Role.STUDENT.name()).stream().map(e -> new VarahamihirGrantedAuthority(e)).collect(Collectors.toSet()))
.scope(Arrays.asList("read", "write").stream().collect(Collectors.toSet()))
.autoApprove(true)
.authorizedGrantTypes(MyConstants.AUTHORIZED_GRANT_TYPES)
.tenantId(tenant.getId())
.build();
clientService.save(clientDetails);
}
}
//setupService.setup();
}
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route(p -> p.path("/*/guardian/**").filters(f ->
f.hystrix(c -> c.setName("guardian").setFallbackUri("forward:/fallback"))).uri("lb://guardian:8081"))
.route(p -> p.path("/*/student/**").filters(f ->
f.hystrix(c -> c.setName("student").setFallbackUri("forward:/fallback")))
.uri("http://student-service:8081")
// .filter(new VarahamihirGatewayRequestPreFilter())
// .filter(new VarahamihirGatewayRequestPostFilter())
)
.build();
}
@RequestMapping("/fallback")
public ResponseEntity<List<String>> fallback() {
System.out.println("fallback enabled");
HttpHeaders headers = new HttpHeaders();
headers.add("fallback", "true");
return ResponseEntity.ok().headers(headers).body(Collections.emptyList());
}
/* @Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}*/
@Bean(name = "passwordEncoder")
public PasswordEncoder passwordEncoder() {
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
return passwordEncoder;
}
/* @Bean(name = "tokenEncoder")
public PasswordEncoder tokenEncoder() {
return new VarahamihirTokenEncoder();
}*/
}

Look at the annotations on the class. The most important annotation is @EnableDiscoveryClient. Since we have to build a number of micro-services and many of the functions are common, we have created couple of modules which we will include in all micro services. The authentication path for identity-server is different than other micro-services because other micro-services will just validate the JWT  token received from the gateway. 
Spring does not recursively import all the application.properties files from child modules, we have to collect really required properties from any child module and manually import them to the application. That is the reason, we have @PropertySource("classpath:jwt.properties"), in the application because this is the properties file where some required properties are stored in one of the child modules. I would have liked if there was some way to instruct Spring to import all the child module's application.properties files recursively.
Apart from these changes, the gateway application is very similar to what we saw in previous post, we have to just pay attention to list of whitelisted URLs in VarahamihirWebServerSecurityConfig. We have basically whitelisted actuator and user registration URLs.

Micro-services

The gateway service (identity-server) is sitting on the edge and is the only entity exposed to internet. It performs complete authentication and authorization cycle. Once the request is accepted by the gateway and is being passed on the other micro-services, they only verify the token and install a security context so that appropriate role based access control can be applied on the endpoints.

We have following roles defined in our system.
package com.avasthi.varahamihir.common.enums;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public enum Role {
USER("user"),
CLIENT("client", false),
TESTER("tester"),
ADMIN("admin", false),
TENANT_ADMIN("tenant_admin", false),
STUDENT("student"),
GUARDIAN("guardian"),
NEWUSER("newUser", false),
REFRESH("refresh", false),
AUTHENTICATE("authenticate", false);
private final String value;
private final boolean allowedOnCreate;
Role(String value) {
this.value = value;
this.allowedOnCreate = true;
}
Role(String value, boolean allowedOnCreate) {
this.value = value;
this.allowedOnCreate = allowedOnCreate;
}
public static Role createFromString(String value) {
for (Role r : Role.values()) {
if (r.value.equalsIgnoreCase(value)) {
return r;
}
}
throw new IllegalArgumentException(value + " is not a valid value");
}
public Set<String> getGrantedAuthority() {
return new HashSet<String>(Arrays.asList(value));
}
public Set<String> getGrantedAuthority(Set<Role> roles) {
return roles.stream().map(r -> r.value).collect(Collectors.toSet());
}
public static Set<String> allowedOnRegister() {
return Arrays.stream(values()).filter(new Predicate<Role>() {
@Override
public boolean test(Role role) {
return role.allowedOnCreate;
}
}).map(r -> r.name()).collect(Collectors.toSet());
}
}
view raw Role.java hosted with ❤ by GitHub

Most of the roles defined in the system are self-explanatory. Some of the roles of interest are as below.
  • ADMIN -- This is like the super-user of the system, will have complete access to all the functions of the system
  • TENANT_ADMIN -- This is like the above role but will only have access to resources of a tenant. This is the admin of a particular tenant
  • REFRESH -- This is an internal role that is assigned to the refresh token issued by the Authorization Server. This role is only allowed to call the refresh endpoint.
  • AUTHENTICATE -- This is the role that is assigned when the user requests a new token using the username and password. 
There are other specific roles for specific types of users and provide them access to their resources.

Authentication

The authentication flow of the microservice starts with few initial filters. Let's look at the order in the security config code.
package com.avasthi.varahamihir.client.configs;
import com.avasthi.varahamihir.client.filters.AuthorizationHeaderFilter;
import com.avasthi.varahamihir.client.filters.TenantHeaderFilter;
import com.avasthi.varahamihir.client.filters.VarahamihirJWTClientAuthWebFilter;
import com.avasthi.varahamihir.client.handlers.VarahamihirClientAuthenticationSuccessHandler;
import com.avasthi.varahamihir.client.services.VarahamihirReactiveAuthTokenUserDetailService;
import com.avasthi.varahamihir.common.constants.VarahamihirConstants;
import com.avasthi.varahamihir.common.exceptions.UnauthorizedException;
import com.avasthi.varahamihir.common.utils.VarahamihirJWTBaseUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
@EnableScheduling
@ComponentScan(basePackages = {"com.avasthi.varahamihir.common.utils", "com.avasthi.varahamihir.common.services"})
public class VarahamihirWebServerSecurityConfig {
@Autowired
private VarahamihirReactiveAuthTokenUserDetailService authTokenUserDetailService;
@Autowired
private VarahamihirJWTBaseUtil jwtUtil;
private static final String[] WHITELISTED_AUTH_URLS = {
"/actuator/health",
"/*/registration/*"
};
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
AuthenticationWebFilter authenticationWebFilter
= new AuthenticationWebFilter(new UserDetailsRepositoryReactiveAuthenticationManager(authTokenUserDetailService));
authenticationWebFilter.setAuthenticationSuccessHandler(new VarahamihirClientAuthenticationSuccessHandler());
return http.csrf().disable()
.authorizeExchange()
.pathMatchers(WHITELISTED_AUTH_URLS).permitAll()
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.anyExchange().authenticated()
.and()
.addFilterAt(new TenantHeaderFilter(), SecurityWebFiltersOrder.FIRST)
.addFilterAt(new AuthorizationHeaderFilter(), SecurityWebFiltersOrder.FIRST)
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.FIRST)
.addFilterAt(new VarahamihirJWTClientAuthWebFilter(jwtUtil), SecurityWebFiltersOrder.HTTP_BASIC)
.build();
}
}

The code is self-explanatory, we first whitelist a bunch of URLs and options and then we add three filters. The first is our good old TenantHeaderFilter that extracts and installs tenant into the SecurityContext. This is very similar to what we saw in previous post as well.
package com.avasthi.varahamihir.client.filters;
import com.avasthi.varahamihir.common.constants.VarahamihirConstants;
import com.avasthi.varahamihir.common.filters.VarahamihirAbstractFilter;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.io.File;
@Component
@Log4j2
public class TenantHeaderFilter extends VarahamihirAbstractFilter implements WebFilter {
private static final String defaultClient = "supersecretclient";
private static final String defaultSecret = "supersecretclient123";
@Value("${tutorial.default.tenant.discriminator:default}")
private String defaultDiscriminator = "default";
@Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
logHeaders(serverWebExchange, webFilterChain);
String requestURI = serverWebExchange.getRequest().getPath().toString();
String[] dirs = requestURI.split(File.separator);
String tenantDiscriminatorInPath = dirs[1];
String tenantDiscriminator = serverWebExchange.getRequest().getHeaders().getFirst(VarahamihirConstants.TENANT_HEADER);
if (requestURI.equals(VarahamihirConstants.HEALTH_ENDPOINT) || tenantDiscriminatorInPath.equals("actuator")) {
tenantDiscriminator = defaultDiscriminator;
tenantDiscriminatorInPath = defaultDiscriminator;
}
if (tenantDiscriminatorInPath.equals(tenantDiscriminator)) {
final String tenantDiscriminatorInContext = tenantDiscriminator;
return webFilterChain
.filter(serverWebExchange)
.subscriberContext(context -> context.put(VarahamihirConstants.TENANT_DISCRIMINATOR_IN_CONTEXT, tenantDiscriminatorInContext));
}
else {
return unauthorizedException(serverWebExchange,
String.format("The header X-tenant should be same as the tenant discriminator in URL. Currently are %s and %s ", tenantDiscriminator, tenantDiscriminatorInPath));
}
}
private void logHeaders(ServerWebExchange exchange, WebFilterChain chain) {
chain.filter(exchange).then(Mono.fromRunnable(()->{
ServerHttpResponse response = exchange.getResponse();
exchange.getRequest().getHeaders().forEach((k,v)->{
log.info(String.format("Request headers : %s: %s", k, v));
});
HttpHeaders headers = response.getHeaders();
headers.forEach((k,v)->{
log.info(String.format("headers : %s: %s", k, v));
System.out.println(k + " : " + v);
});
}));
};
}


The next filter is the AuthorizationHeaderFilter, which extracts the Authorization header from the request and installs it into the SecurityContext.
package com.avasthi.varahamihir.client.filters;
import com.avasthi.varahamihir.common.constants.VarahamihirConstants;
import com.avasthi.varahamihir.common.exceptions.UnauthorizedException;
import com.avasthi.varahamihir.common.filters.VarahamihirAbstractFilter;
import com.avasthi.varahamihir.common.pojos.AuthorizationHeaderValues;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.Base64Utils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
public class AuthorizationHeaderFilter extends VarahamihirAbstractFilter implements WebFilter {
@Value("${tutorial.default.tenant.discriminator:default}")
private String defaultDiscriminator;
@Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
String authorizationHeaderValue
= serverWebExchange.getRequest().getHeaders().getFirst(VarahamihirConstants.AUTHORIZATION_HEADER_NAME);
if (authorizationHeaderValue == null) {
return webFilterChain.filter(serverWebExchange);
}
authorizationHeaderValue = authorizationHeaderValue.trim();
String[] headerPieces = authorizationHeaderValue.split(" ");
String authType = headerPieces[0].toLowerCase();
String authToken = headerPieces[1];
if (authType.equals("bearer")) {
return webFilterChain
.filter(serverWebExchange)
.subscriberContext(context -> context.put(VarahamihirConstants.AUTHORIZATION_HEADER_IN_CONTEXT,
AuthorizationHeaderValues.builder()
.authToken(authToken)
.authType(AuthorizationHeaderValues.AuthType.Bearer)
.build()));
} else if (authType.equals("basic")) {
String[] decodedToken = new String(Base64Utils.decode(authToken.getBytes())).split(":");
String username = decodedToken[0];
String password = decodedToken[1];
return webFilterChain
.filter(serverWebExchange)
.subscriberContext(context -> context.put(VarahamihirConstants.AUTHORIZATION_HEADER_IN_CONTEXT,
AuthorizationHeaderValues.builder()
.authToken(authToken)
.username(username)
.password(password)
.clientId(username)
.authType(AuthorizationHeaderValues.AuthType.Basic)
.build()));
} else {
return unauthorizedException(serverWebExchange, "The authorization header is neither of type basic nor value.");
}
}
}

Now the AuthenticationWebFilter provided by Spring is called which triggers the authentication. This filters requires a authentication manager, for which we use a standard UserDetailsRepositoryReactiveAuthenticationManager. We need to provide a UserDetailService to instantiate the authentication manager.
package com.avasthi.varahamihir.client.services;
import com.avasthi.varahamihir.common.exceptions.UnauthorizedException;
import com.avasthi.varahamihir.common.pojos.TokenClaims;
import com.avasthi.varahamihir.common.pojos.VarahamihirUserDetails;
import com.avasthi.varahamihir.common.utils.VarahamihirJWTBaseUtil;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.proc.BadJOSEException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.text.ParseException;
@Service
public class VarahamihirReactiveAuthTokenUserDetailService
implements ReactiveUserDetailsService, ReactiveUserDetailsPasswordService {
@Value("${varahamihir.dummy.password:12345678901234567890123456789012345678901234567890}")
protected String dummyPassword;
@Autowired
private VarahamihirJWTBaseUtil jwtBaseUtil;
@Override
public Mono<UserDetails> updatePassword(UserDetails userDetails, String s) {
return null;
}
@Override
public Mono<UserDetails> findByUsername(String token) {
try {
TokenClaims tokenClaims = jwtBaseUtil.retrieveTokenClaims(token);
return Mono.just(VarahamihirUserDetails.builder()
.credentialsNonExpired(true)
.accountNonLocked(true)
.grantedAuthorities(tokenClaims.getAuthorities())
.accountMonExpired(true)
.tenantId(tokenClaims.getTenantId())
.password(dummyPassword)
.userName(tokenClaims.getSubject())
.build());
} catch (ParseException | JOSEException | BadJOSEException e) {
Mono.error(new UnauthorizedException(String.format("Token could not parsed and validated.")));
}
return null;
}
}

A standard implementation of UserDetailService requires one to fetch username and password from some persistent store. In our scenario, we already have an authenticated token, so we just provide a dummyPassword that is defined in the above class. All the other parameters to construct a proper UseDetails object can be extracted from the existing token and authentication can proceed.
In this flow, we are only worried about decoding and verifying the JWT token. 
The main authentication function is handled by VarahamihirJWTClientAuthWebFilter
package com.avasthi.varahamihir.client.filters;
import com.avasthi.varahamihir.client.configs.VarahaJWTReactiveClientAuthManager;
import com.avasthi.varahamihir.client.converters.VarahamihirJWTClientAuthConverters;
import com.avasthi.varahamihir.common.utils.VarahamihirJWTBaseUtil;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.WebFilterChainServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class VarahamihirJWTClientAuthWebFilter implements WebFilter {
private VarahamihirJWTBaseUtil jwtUtil;
private final Function<ServerWebExchange, Mono<Authentication>> jwtAuthConverter;
private final ReactiveAuthenticationManager reactiveAuthManager = new VarahaJWTReactiveClientAuthManager();
private ServerSecurityContextRepository securityContextRepository = new WebSessionServerSecurityContextRepository();
private ServerAuthenticationSuccessHandler authSuccessHandler = new WebFilterChainServerAuthenticationSuccessHandler();
public VarahamihirJWTClientAuthWebFilter(VarahamihirJWTBaseUtil jwtUtil) {
this.jwtUtil = jwtUtil;
this .jwtAuthConverter = new VarahamihirJWTClientAuthConverters(jwtUtil);
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return this.getAuthMatcher().matches(exchange)
.filter(matchResult -> matchResult.isMatch())
.flatMap(matchResult -> this.jwtAuthConverter.apply(exchange))
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
.flatMap(token -> authenticate(exchange, chain, token));
}
private ServerWebExchangeMatcher getAuthMatcher(){
List<ServerWebExchangeMatcher> matchers = new ArrayList<>(2);
matchers.add(new PathPatternParserServerWebExchangeMatcher("/**"));
ServerWebExchangeMatcher authMatcher = ServerWebExchangeMatchers.matchers(new OrServerWebExchangeMatcher(matchers));
return authMatcher;
}
private Mono<Void> authenticate(ServerWebExchange exchange,
WebFilterChain chain, Authentication token) {
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
return this.reactiveAuthManager.authenticate(token)
.flatMap(authentication -> onAuthSuccess(authentication, webFilterExchange));
}
private Mono<Void> onAuthSuccess(Authentication authentication, WebFilterExchange webFilterExchange) {
ServerWebExchange exchange = webFilterExchange.getExchange();
SecurityContextImpl securityContext = new SecurityContextImpl();
securityContext.setAuthentication(authentication);
return this.securityContextRepository.save(exchange, securityContext)
.then(this.authSuccessHandler
.onAuthenticationSuccess(webFilterExchange, authentication))
.subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
}}

This class uses a class VarahamihirJWTClientAuthConverters to perform the actual authentication. Both these classes are below. The filter function in VarahamihirJWTClientAuthWebFilter decided if the authentication needs to be done and then authenticate function performs actual authentication.
We use a utility class VarahamihirJWTBaseUtil. In the previous post, we had talked about VarahamihirJWTUtil. Since then, the class is refactored into two classes where the base class has all the decoding related functions and the child class has additional functionality. Many of the beans that we need to create a token are not needed when we want to validate and decode it.
package com.avasthi.varahamihir.client.converters;
import com.avasthi.varahamihir.common.exceptions.UnauthorizedException;
import com.avasthi.varahamihir.common.pojos.TokenClaims;
import com.avasthi.varahamihir.common.utils.VarahamihirJWTBaseUtil;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.proc.BadJOSEException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.text.ParseException;
import java.util.function.Function;
public class VarahamihirJWTClientAuthConverters implements Function<ServerWebExchange, Mono<Authentication>> {
final String bearerLowercase = "bearer";
@Autowired
private VarahamihirJWTBaseUtil jwtUtil;
public VarahamihirJWTClientAuthConverters(VarahamihirJWTBaseUtil jwtUtil) {
this .jwtUtil = jwtUtil;
}
@Override
public Mono<Authentication> apply(ServerWebExchange serverWebExchange) {
try {
String authorizationHeader
= VarahamihirJWTBaseUtil.getAuthorizationPayload(serverWebExchange);
if (authorizationHeader != null) {
String[] pieces = authorizationHeader.split(" ");
if (pieces.length == 2
&& pieces[0].toLowerCase().equals(bearerLowercase)) {
TokenClaims tokenClaims
= jwtUtil.retrieveTokenClaims(pieces[1]);
return Mono.just(jwtUtil.getClientAuthenticationToken(tokenClaims));
}
}
} catch (ParseException | JOSEException | BadJOSEException e) {
e.printStackTrace();
}
return Mono.empty();
}
}

We use a utility class VarahamihirJWTBaseUtil. In the previous post, we had talked about VarahamihirJWTUtil. Since then, the class is refactored into two classes where the base class has all the decoding related functions and the child class has additional functionality. Many of the beans that we need to create a token are not needed when we want to validate and decode it.
package com.avasthi.varahamihir.common.utils;
import com.avasthi.varahamihir.common.constants.VarahamihirConstants;
import com.avasthi.varahamihir.common.enums.VarahamihirSubjectType;
import com.avasthi.varahamihir.common.enums.VarahamihirTokenType;
import com.avasthi.varahamihir.common.exceptions.UnauthorizedException;
import com.avasthi.varahamihir.common.pojos.*;
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.KeyLengthException;
import com.nimbusds.jose.crypto.DirectDecrypter;
import com.nimbusds.jose.crypto.DirectEncrypter;
import com.nimbusds.jose.jwk.source.ImmutableSecret;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.jose.proc.JWEDecryptionKeySelector;
import com.nimbusds.jose.proc.JWEKeySelector;
import com.nimbusds.jose.proc.SimpleSecurityContext;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.proc.BadJWTException;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import java.text.ParseException;
import java.util.*;
import java.util.stream.Collectors;
@Component
@Log4j2
public class VarahamihirJWTBaseUtil {
@Value("${varahamihir.password.encoder.secret}")
protected String secret;
@Value("${varahamihir.claims.issuer:cto@varahamihir.com}")
protected String issuer;
@Value("${varahamihir.dummy.password:MySecretIsReallyReallyBetterThanYourSecretIAmTellingYou}")
protected String dummyPassword;
public JWTClaimsSet getAllClaimsFromToken(String token) throws ParseException, JOSEException, BadJOSEException {
JWTClaimsSet claimsSet = jwtProcessor().process(token, null);
return claimsSet;
}
public String getUsernameFromToken(String token) throws ParseException, JOSEException, BadJOSEException {
return getAllClaimsFromToken(token).getSubject();
}
public Date getExpirationDateFromToken(String token) throws ParseException, JOSEException, BadJOSEException {
return getAllClaimsFromToken(token).getExpirationTime();
}
protected Boolean isTokenExpired(String token) throws ParseException, JOSEException, BadJOSEException {
try {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
catch(Exception e) {
return false;
}
}
public Boolean validateToken(String token) throws ParseException, JOSEException, BadJOSEException {
return !isTokenExpired(token);
}
private ConfigurableJWTProcessor<SimpleSecurityContext> jwtProcessor() {
ConfigurableJWTProcessor<SimpleSecurityContext> jwtProcessor = new DefaultJWTProcessor<SimpleSecurityContext>();
JWKSource<SimpleSecurityContext> jweKeySource = new ImmutableSecret<SimpleSecurityContext>(getSecret());
JWEKeySelector<SimpleSecurityContext> jweKeySelector =
new JWEDecryptionKeySelector<SimpleSecurityContext>(JWEAlgorithm.DIR, EncryptionMethod.A128CBC_HS256, jweKeySource);
jwtProcessor.setJWEKeySelector(jweKeySelector);
return jwtProcessor;
}
public TokenClaims retrieveTokenClaims(String authToken) throws ParseException, JOSEException, BadJOSEException {
try {
JWTClaimsSet claims = getAllClaimsFromToken(authToken);
Set<String> rolesSet = new HashSet<String>((List<String>)claims.getClaims().get(VarahamihirConstants.TOKEN_ROLE_CLAIM));
VarahamihirSubjectType subjectType
= VarahamihirSubjectType.valueOf((String)claims.getClaims().get(VarahamihirConstants.TOKEN_SUBJECT_TYPE));
VarahamihirTokenType tokenType
= VarahamihirTokenType.valueOf((String)claims.getClaims().get(VarahamihirConstants.TOKEN_TYPE_CLAIM));
AbstractTokenRequest.GrantType grantType
= AbstractTokenRequest.GrantType.valueOf((String)claims.getClaims().get(VarahamihirConstants.GRANT_TYPE_CLAIM));
String scope = (String)claims.getClaims().get(VarahamihirConstants.TOKEN_SCOPE);
return TokenClaims.builder()
.authorities(rolesSet.stream().map(s -> new VarahamihirGrantedAuthority(s)).collect(Collectors.toSet()))
.subject(claims.getSubject())
.issuer(claims.getIssuer())
.audience(claims.getAudience())
.authToken(authToken)
.tokenType(tokenType)
.subjectType(subjectType)
.grantType(grantType)
.scope(scope)
.build();
}
catch(BadJWTException e) {
throw new UnauthorizedException(String.format("The token has probably expired or it is bad."));
}
catch(Exception e) {
throw new UnauthorizedException(String.format("Token can't be parsed."));
}
}
protected DirectEncrypter getDirectEncrypter() throws KeyLengthException {
return new DirectEncrypter(getSecret());
}
private DirectDecrypter getDirectDecrypter() throws KeyLengthException {
return new DirectDecrypter(getSecret());
}
private byte[] getSecret() {
int keyBitLength = EncryptionMethod.A128CBC_HS256.cekBitLength();
return secret.substring(0, keyBitLength/8).getBytes();
}
public static String getAuthorizationPayload(ServerWebExchange serverWebExchange) {
return serverWebExchange.getRequest()
.getHeaders()
.getFirst(HttpHeaders.AUTHORIZATION);
}
public Authentication getAuthenticationToken(TokenClaims tokenClaims, String authToken) {
VarahamihirOAuth2Principal principal
= VarahamihirOAuth2Principal.builder()
.username(tokenClaims.getSubject())
.authToken(authToken)
.tokenType(tokenClaims.getTokenType())
.subjectType(tokenClaims.getSubjectType())
.audience(tokenClaims.getAudience())
.grantType(tokenClaims.getGrantType())
.scope(tokenClaims.getScope())
.grantedAuthorities(tokenClaims.getAuthorities().stream().map(a -> a.getAuthority()).collect(Collectors.toSet()))
.build();
return new UsernamePasswordAuthenticationToken(principal, authToken, tokenClaims.getAuthorities());
}
public Authentication getClientAuthenticationToken(TokenClaims tokenClaims) {
VarahamihirTokenPrincipal principal
= VarahamihirTokenPrincipal.builder()
.username(tokenClaims.getSubject())
.credentials(dummyPassword)
.authToken(tokenClaims.getAuthToken())
.grantedAuthorities(tokenClaims.getAuthorities().stream().map(a -> a.getAuthority()).collect(Collectors.toSet()))
.scope(tokenClaims.getScope())
.build();
return new UsernamePasswordAuthenticationToken(principal, tokenClaims.getAuthToken(), tokenClaims.getAuthorities());
}
}

Communication

Since we will be using OpenFeign for communication across micro-services, we define this in this common module as well.
We have three micro-services and we expose the interfaces related to each of them that are required for communication.
package com.avasthi.varahamihir.client.proxies;
import com.avasthi.varahamihir.client.configs.VarahamihirFeignClientConfig;
import com.avasthi.varahamihir.common.constants.VarahamihirConstants;
import com.avasthi.varahamihir.common.pojos.UserPojo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
@FeignClient(value = "identity-service",
configuration = VarahamihirFeignClientConfig.class)
public interface VarahamihirIdentityServerUserClient {
@RequestMapping(method = RequestMethod.GET,
value = VarahamihirConstants.V1_USER_ENDPOINT + "/{userId}",
produces = MediaType.APPLICATION_JSON_VALUE)
UserPojo getUser(@RequestHeader(value = VarahamihirConstants.AUTHORIZATION_HEADER_NAME, required = true) String authorizationHeader,
@RequestHeader(value = VarahamihirConstants.TENANT_HEADER, required = true) String tenantheader,
@PathVariable("tenant") String tenant,
@PathVariable("userId") String userId);
@RequestMapping(method = RequestMethod.POST,
value = VarahamihirConstants.V1_REGISTRATION_ENDPOINT + "/user",
produces = MediaType.APPLICATION_JSON_VALUE)
UserPojo createUser(@RequestHeader(value = VarahamihirConstants.TENANT_HEADER, required = true) String tenantheader,
@PathVariable("tenant") String tenant,
@RequestBody UserPojo userPojo);
}

This the interface to identity server and provides two services, given a username, it can get you the details of that user and it can create a user. The second is required because when the other micro-services need to create a student or a guardian, they first need to create a user and then create student or guardian. The other micro-services also make sure appropriate roles are applied to users based on their function. The external endpoint doesn't allow one to provide roles.
package com.avasthi.varahamihir.client.proxies;
import com.avasthi.varahamihir.client.configs.VarahamihirFeignClientConfig;
import com.avasthi.varahamihir.common.constants.VarahamihirConstants;
import com.avasthi.varahamihir.common.pojos.GuardianPojo;
import com.avasthi.varahamihir.common.pojos.RegistrationPojo;
import com.avasthi.varahamihir.common.pojos.UserPojo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
@FeignClient(value = "guardian-service",
configuration = VarahamihirFeignClientConfig.class)
public interface VarahamihirGuardianUserClient {
@RequestMapping(method = RequestMethod.GET,
value = VarahamihirConstants.V1_GUARDIAN_ENDPOINT + "/{guardianId}",
produces = MediaType.APPLICATION_JSON_VALUE)
GuardianPojo getGuardian(@RequestHeader(value = VarahamihirConstants.AUTHORIZATION_HEADER_NAME, required = true) String authorizationHeader,
@RequestHeader(value = VarahamihirConstants.TENANT_HEADER, required = true) String tenantheader,
@PathVariable("tenant") String tenant,
@PathVariable("guardianId") String guardianId);
}

The student service interface provides two interfaces, one to get the details of a student and another to create a student. This will be called from the guardian endpoint since we envisage that a guardian will create students.
package com.avasthi.varahamihir.client.proxies;
import com.avasthi.varahamihir.client.configs.VarahamihirFeignClientConfig;
import com.avasthi.varahamihir.common.constants.VarahamihirConstants;
import com.avasthi.varahamihir.common.pojos.GuardianPojo;
import com.avasthi.varahamihir.common.pojos.RegistrationPojo;
import com.avasthi.varahamihir.common.pojos.StudentPojo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
@FeignClient(value = "student-service",
configuration = VarahamihirFeignClientConfig.class)
public interface VarahamihirStudentUserClient {
@RequestMapping(method = RequestMethod.GET,
value = VarahamihirConstants.V1_USER_ENDPOINT + "/{studentId}",
produces = MediaType.APPLICATION_JSON_VALUE)
StudentPojo getStudent(@RequestHeader(value = VarahamihirConstants.AUTHORIZATION_HEADER_NAME, required = true) String authorizationHeader,
@RequestHeader(value = VarahamihirConstants.TENANT_HEADER, required = true) String tenantheader,
@PathVariable("tenant") String tenant,
@PathVariable("guardianId") String guardianId);
@RequestMapping(method = RequestMethod.POST,
value = VarahamihirConstants.V1_STUDENT_ENDPOINT,
produces = MediaType.APPLICATION_JSON_VALUE)
StudentPojo createStudent(@RequestHeader(value = VarahamihirConstants.AUTHORIZATION_HEADER_NAME, required = true) String authorizationHeader,
@RequestHeader(value = VarahamihirConstants.TENANT_HEADER, required = true) String tenantheader,
@PathVariable("tenant") String tenant,
@RequestBody RegistrationPojo registrationPojo);
}

The guardian service just provides a single interface and that allows one to query details of a guardian.
We also need to provide a config for all the Feign clients.

package com.avasthi.varahamihir.client.configs;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Logger;
import feign.Request;
import feign.codec.Decoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import java.util.concurrent.TimeUnit;
@Configuration
public class VarahamihirFeignClientConfig {
@Value("${feign.connectTimeout:10000}")
private int connectTimeout;
@Value("${feign.readTimeOut:300000}")
private int readTimeout;
@Bean
public Decoder feignDecoder() {
return new ResponseEntityDecoder(new SpringDecoder(httpMessageConvertersObjectFactory()));
}
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public Request.Options options() {
return new Request.Options(connectTimeout, TimeUnit.MILLISECONDS, readTimeout, TimeUnit.MILLISECONDS, true);
}
public ObjectMapper customObjectMapper(){
ObjectMapper objectMapper = new ObjectMapper();
//Customize as much as you want
return objectMapper;
}
@Bean
public ObjectFactory<HttpMessageConverters> httpMessageConvertersObjectFactory() {
HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(customObjectMapper());
ObjectFactory<HttpMessageConverters> objectFactory = () -> new HttpMessageConverters(jacksonConverter);
return objectFactory;
}
@Bean
public HttpMessageConverters messageConverters() {
return httpMessageConvertersObjectFactory().getObject();
}
}

Implementing micro-service

Now that we have the common module completed, we can create our micro-services modules and implement the actual business logic. Here I present one sample of how the micro-service looks like. We take the example of Student micro-service.

We first define an endpoint class, in this class our endpoint has two interfaces, a GET and a POST.
package com.avasthi.varahamihir.student.endpoints;
import com.avasthi.varahamihir.common.annotations.AdminTenantAdminOrCurrentUserBodyPermission;
import com.avasthi.varahamihir.common.annotations.AdminTenantAdminOrGuardianPermission;
import com.avasthi.varahamihir.common.constants.VarahamihirConstants;
import com.avasthi.varahamihir.common.endpoints.v1.BaseEndpoint;
import com.avasthi.varahamihir.common.pojos.RegistrationPojo;
import com.avasthi.varahamihir.common.pojos.StudentPojo;
import com.avasthi.varahamihir.common.pojos.VarahamihirTokenPrincipal;
import com.avasthi.varahamihir.student.service.StudentService;
import io.swagger.annotations.Api;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
/**
* Created by vinay on 1/4/16.
*/
@Log4j2
@RestController
@RequestMapping(VarahamihirConstants.V1_STUDENT_ENDPOINT)
@Api(value="Student endpoint")
public class StudentEndpoint extends BaseEndpoint {
@Autowired
private StudentService studentService;
@RequestMapping(value = "/{studentname}",
method = RequestMethod.GET,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
@AdminTenantAdminOrCurrentUserBodyPermission
public Mono<StudentPojo> getStudent(@PathVariable("studentname") String username,
Authentication authentication) {
VarahamihirTokenPrincipal principal
= (VarahamihirTokenPrincipal)authentication.getPrincipal();
return studentService.getStudent(String.format("Bearer %s", principal.getAuthToken()), username);
}
@RequestMapping(method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
@AdminTenantAdminOrGuardianPermission
public Mono<StudentPojo> createStudent(@RequestBody RegistrationPojo registrationPojo,
Authentication authentication) {
VarahamihirTokenPrincipal principal
= (VarahamihirTokenPrincipal)authentication.getPrincipal();
return studentService.createStudent(String.format("Bearer %s", principal.getAuthToken()), registrationPojo);
}
}
Now we define a service layer that will implement the functionality required by the endpoint.
package com.avasthi.varahamihir.student.service;
import com.avasthi.varahamihir.client.proxies.VarahamihirGuardianUserClient;
import com.avasthi.varahamihir.client.proxies.VarahamihirIdentityServerUserClient;
import com.avasthi.varahamihir.client.services.VarahamihirAbstractFeignClientService;
import com.avasthi.varahamihir.common.constants.VarahamihirConstants;
import com.avasthi.varahamihir.common.enums.Role;
import com.avasthi.varahamihir.common.exceptions.*;
import com.avasthi.varahamihir.common.pojos.GuardianPojo;
import com.avasthi.varahamihir.common.pojos.RegistrationPojo;
import com.avasthi.varahamihir.common.pojos.StudentPojo;
import com.avasthi.varahamihir.common.pojos.UserPojo;
import com.avasthi.varahamihir.student.entities.Student;
import com.avasthi.varahamihir.student.mappers.StudentPojoMapper;
import com.avasthi.varahamihir.student.repositories.StudentRepository;
import feign.FeignException;
import io.fabric8.openshift.api.model.User;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
@Log4j2
@Service
public class StudentService extends VarahamihirAbstractFeignClientService {
@Autowired
private VarahamihirIdentityServerUserClient identityServerUserClient;
@Autowired
private VarahamihirGuardianUserClient guardianServiceClient;
@Autowired
private StudentRepository studentRepository;
@Autowired
private StudentPojoMapper studentPojoMapper;
public Mono<StudentPojo> getStudent(String authorizationHeader,
String username) {
return Mono.subscriberContext()
.flatMap(tenantDiscriminatorContext -> {
String tenantDiscriminator
= tenantDiscriminatorContext.<String>get(VarahamihirConstants.TENANT_DISCRIMINATOR_IN_CONTEXT);
UserPojo user = null;
try {
user = identityServerUserClient.getUser(authorizationHeader, tenantDiscriminator, tenantDiscriminator, username);
}
catch(FeignException fex) {
return Mono.error(convertFeignException(fex, username, tenantDiscriminator));
}
Optional<Student> optionalStudent = studentRepository.findById(user.getId());
if (optionalStudent.isPresent()) {
return Mono.just(studentPojoMapper.convert(optionalStudent.get()));
} else {
return Mono.error(new UnauthorizedException(String.format("Student doesn't exist with username", username)));
}
}).switchIfEmpty(Mono.error(new EntityNotFoundException(String.format("User %s is not registered.", username))))
.doOnError(e -> log.error("Identity Server replica not available."+ e.toString(), e))
.onErrorResume((e -> Mono.error(raiseAppropriateException((Exception) e))));
}
public Mono<StudentPojo> createStudent(String authorizationHeader,
RegistrationPojo registrationPojo) {
return Mono.subscriberContext()
.flatMap(tenantDiscriminatorContext -> {
String tenantDiscriminator
= tenantDiscriminatorContext.<String>get(VarahamihirConstants.TENANT_DISCRIMINATOR_IN_CONTEXT);
try {
UserPojo userPojo
= UserPojo.builder()
.password(registrationPojo.getPassword())
.fullname(registrationPojo.getFullname())
.email(registrationPojo.getEmail())
.username(registrationPojo.getUsername())
.grantedAuthorities(new HashSet<>(Arrays.asList(Role.USER.name(), Role.STUDENT.name())))
.build();
userPojo = identityServerUserClient.createUser(tenantDiscriminator, tenantDiscriminator, userPojo);
registrationPojo.setId(userPojo.getId());
registrationPojo.setTenantId(userPojo.getTenantId());
Student student
= Student.builder()
.userId(registrationPojo.getId())
.tenantId(registrationPojo.getTenantId())
.guardianId(registrationPojo.getGuardianId())
.build();
student = studentRepository.save(student);
StudentPojo studentPojo = studentPojoMapper.convert(student);
studentPojo.setUser(userPojo);
return Mono.just(studentPojo);
}
catch(FeignException fex) {
return Mono.error(convertFeignException(fex, registrationPojo.getUsername(), tenantDiscriminator));
}
}).switchIfEmpty(Mono.error(new EntityNotFoundException(String.format("User %s is not registered.", registrationPojo.getUsername()))))
.doOnError(e -> log.error("Identity Server replica not available."+ e.toString(), e))
.onErrorResume((e -> Mono.error(raiseAppropriateException((Exception) e))));
}
}

Next we define a repository.
package com.avasthi.varahamihir.student.repositories;
import com.avasthi.varahamihir.student.entities.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface StudentRepository extends JpaRepository<Student, UUID> {
}

Next we define a mapper class that can convert entities to Pojo. This is not really required but is useful if what is exposed over JSON is remarkably different than what is stored in database.
package com.avasthi.varahamihir.student.mappers;
import com.avasthi.varahamihir.common.pojos.StudentPojo;
import com.avasthi.varahamihir.student.entities.Student;
import org.springframework.stereotype.Component;
@Component
public class StudentPojoMapper {
public StudentPojo convert(Student s) {
return StudentPojo.builder()
.userId(s.getUserId())
.tenantId(s.getTenantId())
.createdAt(s.getCreatedAt())
.createdBy(s.getCreatedBy())
.updatedBy(s.getUpdatedBy())
.guardianId(s.getGuardianId())
.build(); // Not copying password into pojo
}
public Student convert(StudentPojo s) {
return Student.builder()
.userId(s.getUserId())
.createdAt(s.getCreatedAt())
.createdBy(s.getCreatedBy())
.tenantId(s.getTenantId())
.guardianId(s.getGuardianId())
.updatedAt(s.getUpdatedAt())
.updatedBy(s.getUpdatedBy())
.build();
}
}

Finally, our application. Make sure all the annotations are in place.
package com.avasthi.varahamihir.student;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.avasthi.varahamihir.common.services.DateTimeService;
import io.dekorate.kubernetes.annotation.ImagePullPolicy;
import io.dekorate.kubernetes.annotation.KubernetesApplication;
import io.dekorate.kubernetes.annotation.Probe;
import io.dekorate.kubernetes.annotation.ServiceType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.reactive.config.EnableWebFlux;
import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.List;
@EnableWebFlux
@SpringBootApplication(scanBasePackages = {"com.avasthi.varahamihir"})
@Configuration
@EnableDiscoveryClient
@EntityScan(basePackages = {"com.avasthi.varahamihir"})
@ComponentScan(basePackages = {"com.avasthi.varahamihir"})
@EnableJpaRepositories("com.avasthi.varahamihir")
@EnableFeignClients(basePackages = {"com.avasthi.varahamihir.client.proxies"})
//@EnableSwagger2
@KubernetesApplication(livenessProbe = @Probe(httpActionPath = "/actuator/health"),
readinessProbe = @Probe(httpActionPath = "/actuator/health"),
serviceType = ServiceType.NodePort,
imagePullPolicy = ImagePullPolicy.Always)
@PropertySource("classpath:jwt.properties")
public class StudentLauncher {
@Autowired
private DateTimeService dateTimeService;
public static void main(String[] args) {
SpringApplication.run(StudentLauncher.class, args);
}
@PostConstruct
public void initialize() {
}
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public Module jodaModule() {
return new JodaModule();
}
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route(p -> p.path("/*/identity-server/**").filters(f ->
f.hystrix(c -> c.setName("identity-server").setFallbackUri("forward:/fallback"))).uri("lb://identity-server:8081"))
.route(p -> p.path("/*/student/**").filters(f ->
f.hystrix(c -> c.setName("student").setFallbackUri("forward:/fallback"))).uri("lb://student:8081"))
.build();
}
@RequestMapping("/fallback")
public ResponseEntity<List<String>> fallback() {
System.out.println("fallback enabled");
HttpHeaders headers = new HttpHeaders();
headers.add("fallback", "true");
return ResponseEntity.ok().headers(headers).body(Collections.emptyList());
}
}

Deployment

First thing we need to do is the bundle the application in docker images. We need to make sure our pom.xml has following directive.
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot-dependencies.version}</version>
<configuration>
<image>
<name>gcr.io/my-google-project/${project.artifactId}:${project.version}</name>
</image>
</configuration>
<executions>
<execution>
<goals>
<goal>build-image</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.plugin.version}</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>${docker-maven-plugin.version}</version>
</plugin>
</plugins>
</build>
view raw pom-docker.xml hosted with ❤ by GitHub

The build-image goal in spring-boot-maven-plugin makes sure that on mvn clean package -DskipTests command creates a docker image. We have prefixed the image with gcr.io because we want to use google cloud and if we push that image, it will be available in google container registery. 
Now we need to generate yaml files for each of our applications. Even though I used dekorate to automatically generate yaml files, I always had to manually modify them. So I finally decided to create my own yaml files.
Since I am using Google Cloud SQL, I am using the sidecar approach to deploy the cloud sql proxy.  Since the database requires authentication, I am using the kubernetes secret to define that.
We use Workload Identity method for the application to work in google cloud. Here are the steps that need to be followed for that.
Go to google cloud container page and create a cluster for yourself. You will also need to create a google project before that. Make a note of the cluster name.
Set your google cloud project in your shell and create a service account.
$ gcloud iam service-accounts create my_gsa_name
$ kubectl create namespace my_namespace
$ kubectl create serviceaccount --namespace my_namespace my_ksa
  
Now associate policy bindings and annotate the account
$ gcloud config set project my_project
$ gcloud iam service-accounts add-iam-policy-binding \
  --role roles/iam.workloadIdentityUser \
  --member "serviceAccount:cluster_project.svc.id.goog[my_namespace/my_ksa]" \
  my_gsa_name@gsa_project.iam.gserviceaccount.com
You can leave my_namespace to default if you wish. my_ksa is your kubernetes service account which is tied to your my_gsa_name google account. Since we are going to use Cloud SQL, you need to make sure the my_gsa_name has one of the Cloud roles into it. Otherwise you will get errors related to permissions for using Cloud SQL.
Now we need to complete the binding between KSA and GSA.
$ kubectl annotate serviceaccount \
  --namespace my_namespace \
   my_ksa \
   iam.gke.io/gcp-service-account=my_gsa_name@gsa_project.iam.gserviceaccount.com
You also need to create a secret for database that the yaml file will use to authenticate with cloud sql. This secret is used within the yaml file for service deployment.
$ kubectl create secret generic my-secret \
  --from-literal=username=my-db-user \
  --from-literal=password=MyDbPassword \
  --from-literal=database=my-db-name


Here is the yaml file for one of the micro-services. Similarly one can write for other micro-services as well.
apiVersion: apps/v1
kind: Deployment
metadata:
name: identity-server
spec:
selector:
matchLabels:
app: identity-server
template:
metadata:
labels:
app: identity-server
spec:
serviceAccountName: my-ksa
containers:
- env:
- name: "KUBERNETES_NAMESPACE"
valueFrom:
fieldRef:
fieldPath: "metadata.namespace"
name: identity-server
image: "gcr.io/my-cloud/identity-server:0.0.1-SNAPSHOT"
imagePullPolicy: "Always"
livenessProbe:
failureThreshold: 3
httpGet:
path: "/actuator/health"
port: 8081
scheme: "HTTP"
initialDelaySeconds: 0
periodSeconds: 30
successThreshold: 1
timeoutSeconds: 10
name: "identity-server"
ports:
- containerPort: 8081
name: "http"
protocol: "TCP"
readinessProbe:
failureThreshold: 3
httpGet:
path: "/actuator/health"
port: 8081
scheme: "HTTP"
initialDelaySeconds: 0
periodSeconds: 30
successThreshold: 1
timeoutSeconds: 10
env:
- name: DB_USER
valueFrom:
secretKeyRef:
name: my-secret
key: username
- name: DB_PASS
valueFrom:
secretKeyRef:
name: my-secret
key: password
- name: DB_NAME
valueFrom:
secretKeyRef:
name: my-secret
key: database
- name: cloud-sql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.17
command: [ "/cloud_sql_proxy", "-instances=my-cloud:us-central1:my-sql=tcp:3306", "-ip_address_types=PRIVATE" ]
securityContext:
runAsNonRoot: true

Now that we are ready with all the stuff, we just need to call mvn clean package -DskipTests. This will create all the docker images. Now we need to push these docker images to Google Container Registry. For this, we need to call docker push on each of the images. I use following script to automate the complete deployment operation.
#!/bin/bash
IMAGES=`docker images --format "table {{.Repository}},{{.ID}},{{.Repository}},{{.Tag}},{{.Size}}" | grep 'identity-server\|student\|guardian' |cut -d ',' -f 2`
[ $? -eq 0 ]  || exit 1
docker image rm -f $IMAGES
mvn clean package -DskipTests
[ $? -eq 0 ]  || exit 1
docker push gcr.io/varahamihir-cloud/student
[ $? -eq 0 ]  || exit 1
docker push gcr.io/varahamihir-cloud/guardian
[ $? -eq 0 ]  || exit 1
docker push gcr.io/varahamihir-cloud/identity-server
[ $? -eq 0 ]  || exit 1
kubectl -n service rollout restart --namespace=varahamihir-k8s-ns deployment student
[ $? -eq 0 ]  || exit 1
kubectl -n service rollout restart --namespace=varahamihir-k8s-ns deployment guardian
[ $? -eq 0 ]  || exit 1
kubectl -n service rollout restart --namespace=varahamihir-k8s-ns deployment identity-server
[ $? -eq 0 ]  || exit 1
Now that the servers are deployed, we can check the condition of all the pods.
$ kubectl get pods --namespace=my-namespace
NAME                               READY   STATUS    RESTARTS   AGE
guardian-7c75fcb9c8-txg4q          2/2     Running   0          3h6m
identity-server-67d7878db8-47lqq   2/2     Running   0          3h6m
student-68485fb749-l65gc           2/2     Running   1          3h6m
As we can see, each of the pods are running two workloads because the cloud_sql_proxy is running as a sidecar for each of the pods. The benefit of this is that we can use the cloud sql database as localhost. We can see the logs of any of the processes by following command.

$ kubectl logs --namespace=varahamihir-k8s-ns pods/student-5bf4ccd6cc-5g29q --container student 
Error from server (NotFound): pods "student-5bf4ccd6cc-5g29q" not found
vavasthi@VinayLinux-Desktop:~/work/varahamihir$ kubectl logs --namespace=varahamihir-k8s-ns pods/student-68485fb749-l65gc --container student                         
Container memory limit unset. Configuring JVM for 1G container.
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -XX:MaxMetaspaceSize=216643K -XX:ReservedCodeCacheSize=240M -Xss1M -Xmx524732K (Head Room: 0%, Loaded Class Count: 35835, Thread Count: 50, Total Memory: 1073741824)
Adding 127 container CA certificates to JVM truststore

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.1.RELEASE)
Now that we are ready, we can see our micro-services in action. First let's create a guardian in the system.
$ curl --request POST --url http://35.225.145.174/default/registration/guardian --header cache-control: no-cache --header content-type: application/json --header x-tenant: default --data {"username" : "testguardian","password" : "testguardian1123","fullname" : "My Guardian","email":"testguardian1@varahamihir.com"}
{
    "tenantId": "738f6863-32dc-4a92-9f3a-cad3ef38fe6f",
    "user": {
        "email": "testguardian1123",
        "grantedAuthorities": [
            "GUARDIAN",
            "USER"
        ],
        "id": "5d6a6e08-0a88-4c5f-9bc1-b220ece79397",
        "mask": 0,
        "tenantId": "738f6863-32dc-4a92-9f3a-cad3ef38fe6f",
        "username": "testguardian"
    },
    "userId": "5d6a6e08-0a88-4c5f-9bc1-b220ece79397"
}
Now we can authenticate using this guardian.
$ curl --request POST \
  --url http://35.225.145.174/default/oauth/token \
  --header 'authorization: Basic c3VwZXJzZWNyZXRjbGllbnQ6c3VwZXJzZWNyZXRjbGllbnQxMjM=' \
  --header 'cache-control: no-cache' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --header 'postman-token: 26951e8d-bbe2-f617-896d-9a31658fe142' \
  --header 'x-tenant: default' \
  --data 'username=testguardian&password=testguardian1123&audience=self&grant_type=password'
  {
    "auth_token": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..6p7meCoT0JyW9dKjeM_h3w.1bjKuj_5nVS_a7tC8zX6euEXyarrrQNCm62ImgrTxCw68oc7leQsAPh5TYyNpyGSE3pdsKCAie67c8WW13dBpRcw22BbitGTKdabpo8L3z5Y51YB7wWx_lQuSfXkTfd9haa2OmnpUjYyNMdHlu6QqGx4fI7iIOC0a4Ir4SkeF6jLMlpxyqpMkPyeZuQZHZDyjT9Cyo8bTFkaNZ8wPy_PX-nCWZROjQiXUQ33ChQ5Uy3QkBz0CCvAOMUjDyIuIA-yH2O35HkCsX44yZ2w-DDjzNyYe5WJIF1iuTpSQfLtkG0jvqkZUp5HyD6wwB5WcvbDzj-wOV7cuVEvY1BoFfUXEjfeZ-pH45_WWV05PeKyiCA.2xBZDLa9VQqiOUGK_pESuA",
    "expiry": 500,
    "refreshToken": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..FttlYRPM4nwViMyZELXnsQ.ZRklt6EClyJCI3lxaIr_W84mtMfdoZUlzWVkM-85tDz8-IwhRjtzieRfErzcuWJB1MB2Y2m4QtVRuLMagilWqc_CbLYb31SOLlMDfHN6TOJck-qpFg_qziaofFOFVY6idlUP9GO2-nuDjROXJLhjFC47mbwOrfqdGoWXAqoaBJzhgKRnKcWJKG_COjN5uXhnirCYM_bc3bI8HJG_1i_Ca_7wgDadSOIyXwS9-DqpqMjMBLZUyIe9S_jqwG7RZox1Ll_MWv7DZvYvhk8ouFZSkB5tL7qI2tNoEPNre_rICfSxNIgJmx_YeI-4IgvQyAbG6SanEDqEfEmRowjbQJ9GCw.-QPY9_YOFllm9yqb-B1WuQ",
    "refreshTokenExpiry": 1000,
    "scope": ",",
    "tokenType": "Bearer"
}
Now using the auth_token, the guardian can add one student to himself.
$ curl --request POST \
  --url http://35.225.145.174/default/guardian/testguardian/student \
  --header 'authorization: Bearer eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..6p7meCoT0JyW9dKjeM_h3w.1bjKuj_5nVS_a7tC8zX6euEXyarrrQNCm62ImgrTxCw68oc7leQsAPh5TYyNpyGSE3pdsKCAie67c8WW13dBpRcw22BbitGTKdabpo8L3z5Y51YB7wWx_lQuSfXkTfd9haa2OmnpUjYyNMdHlu6QqGx4fI7iIOC0a4Ir4SkeF6jLMlpxyqpMkPyeZuQZHZDyjT9Cyo8bTFkaNZ8wPy_PX-nCWZROjQiXUQ33ChQ5Uy3QkBz0CCvAOMUjDyIuIA-yH2O35HkCsX44yZ2w-DDjzNyYe5WJIF1iuTpSQfLtkG0jvqkZUp5HyD6wwB5WcvbDzj-wOV7cuVEvY1BoFfUXEjfeZ-pH45_WWV05PeKyiCA.2xBZDLa9VQqiOUGK_pESuA' \
  --header 'cache-control: no-cache' \
  --header 'content-type: application/json' \
  --header 'postman-token: 2e9901da-ef01-a3bb-ba28-1f0dff5f7381' \
  --header 'x-tenant: default' \
  --data '{\n	"username" : "teststudent1",\n	"password" : "teststudent123",\n	"fullname" : "Test Student",\n	"email":"teststudent@varahamihir.com",\n	"guardianName":"testguardian"\n}'
      "userId": "45fe4888-2a64-48bd-bea7-ad512f0ef724",
    "tenantId": "738f6863-32dc-4a92-9f3a-cad3ef38fe6f",
    "guardianId": "5d6a6e08-0a88-4c5f-9bc1-b220ece79397",
    "user": {
        "id": "45fe4888-2a64-48bd-bea7-ad512f0ef724",
        "tenantId": "738f6863-32dc-4a92-9f3a-cad3ef38fe6f",
        "fullname": "Test Student",
        "username": "teststudent1",
        "email": "teststudent@varahamihir.com",
        "mask": 0,
        "grantedAuthorities": [
            "STUDENT",
            "USER"
        ]
    }
}
  
So we can see the system coordinating across three micro-services. The communication can also be effective since the communication is happening across local ip addresses and doesn't have to go across the external load balancer. Each of the micro-services are also not aware of the ip addresses of each of the pieces and Kubernetes Discovery Service takes care of it.
Complete code synchronized with this blog post is available in my repository tagged as v1.1.


No comments:

Post a Comment

Spring Microservices with Kubernetes on Google, Ribbon, Feign and Spring Cloud Gateway

In the previous post, we built a JWT authentication server. Now we will build on that server and create an initial instance of the applicat...