Microservice architecture | 7.2 building OAuth2 security authentication using JWT token storage

Posted by Katmando on Thu, 03 Feb 2022 10:52:20 +0100

preface

<Spring Microservices in Action>
Principle and practice of Spring Cloud Alibaba microservice
"Spring cloud framework development tutorial in Silicon Valley of station B" Zhou Yang

JWT provides specifications and standards for OAuth2 tokens, and JWT tokens can be customized;

1. Basic knowledge of JWT token storage

1.1 JSON Web Token

  • OAuth2 is a token based authentication framework, but it does not provide any standard for how to define tokens in its specification;
  • Thus, a new JSON web token (JWT) standard appears;
  • JWT is an open standard (RFC-7519) proposed by the Internet Engineering Task Force (IETF), which aims to provide a standard structure for 0Auth2 tokens
    • JWT token features: compact, password signature, self-contained and extensible;
  • Spring Cloud Security provides out of the box support for JWT;

2. Build OAuth2 server using JWT token storage

2.1 introduction of POM XML dependency file

<!-- JWT OAuth2 library -->
<dependency> 
  <groupid>org.springframework.security</groupid> 
  <artifactid>spring-security-jwt</artifactid> 
</dependency>

<!--security General security library-->
<dependency> 
  <groupid>org.springframework.cloud</groupid> 
  <artifactid>spring-cloud-security</artifactid> 
</dependency> 
<!--oauth2.0-->
<dependency>
  <groupId>org.springframework.security.oauth</groupId>
  <artifactId>spring-security-oauth2</artifactId>
</dependency>

2.2 create JWT token store

Under security package or config package;

@Configration
public class JWTTokenStoreConfig{
    @Autowired
    private ServiceConfig serviceConfig;

    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
 
    //Used to read data from the token presented to the service
    @Bean
    @Primary  //It is used to tell Spring that if there are multiple beans of specific types (DefaultTokenService in this case), the Bean type marked by @ Primary will be automatically injected
    public DefaultTikenServices tokenServices(){
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }
    
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        //Act as a translator between JWT and OAuth2 servers 
        JwtAccessTokenConverter conver = new JwtAccessTokenConverter();
        //Define the signing key that will be used to sign the token
        conver.setSigningKey(serviceConfig.getJwtSigningKey());
    } 

    @Bean
    public TokenEnhancer jwtTokenEnhancer(){
        return new JWTTokenEnhancer();
    }

}
  • This class is used to define how Spring will manage the creation, signature and translation of JWT tokens;

2.3 mount JWT into the authentication service

@Configuration
public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private DefaultTokenServices tokenServices;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Autowired
    private TokenEnhancer jwtTokenEnhancer;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));

        endpoints.tokenStore(tokenStore)                             //The command store defined in JWT, 5.2 will be injected here
                .accessTokenConverter(jwtAccessTokenConverter)       //JWT, hook, used to tell Spring Security OAuth2 code to use JWT
                .tokenEnhancer(tokenEnhancerChain)                   //JWT
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        clients.inMemory()
                .withClient("eagleeye")
                .secret("thisissecret")
                .authorizedGrantTypes("refresh_token", "password", "client_credentials")
                .scopes("webclient", "mobileclient");
    }
}

3. Use JWT in protected services

3.1 introduction of POM XML dependency file

  • It depends on OAuth2 server;
<!-- JWT OAuth2 library -->
<dependency> 
    <groupid>org.springframework.security</groupid> 
    <artifactid>spring-security-jwt</artifactid> 
</dependency>

<!--security General security library-->
<dependency> 
	<groupid>org.springframework.cloud</groupid> 
	<artifactid>spring-cloud-security</artifactid> 
</dependency> 
<!--oauth2.0-->
<dependency>
	<groupId>org.springframework.security.oauth</groupId>
	<artifactId>spring-security-oauth2</artifactId>
</dependency>

3.2 create JWTTokenStoreConfig class in the protected service

  • The same as 5.2 creating JWT token storage in this chapter;
@Configuration
public class JWTTokenStoreConfig {

    @Autowired
    private ServiceConfig serviceConfig;
    //JWT
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    //JWT
    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }

    //JWT
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(serviceConfig.getJwtSigningKey());
        return converter;
    }

}

3.3 create a custom RestTemplate class to inject JWT token

It can be in the main program class, or in the package where the main program class is located and its sub packages;

@Primary
@Bean
public RestTemplate getCustomRestTemplate() {
    RestTemplate template = new RestTemplate();
    List interceptors = template.getInterceptors();
    if (interceptors == null) {
        //UserContextInterceptor will inject the Authorization header into each REST call
        template.setInterceptors(Collections.singletonList(new UserContextInterceptor()));
    } else {
        interceptors.add(new UserContextInterceptor());
        template.setInterceptors(interceptors);
    }
    return template;
}

3.4 UserContextInterceptor will inject JWT token into REST call

public class UserContextInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(
            HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {

        HttpHeaders headers = request.getHeaders();
        headers.add(UserContext.CORRELATION_ID, UserContextHolder.getContext().getCorrelationId());
        //Add authorization token to HTTP header
        headers.add(UserContext.AUTH_TOKEN, UserContextHolder.getContext().getAuthToken());

        return execution.execute(request, body);
    }
}

3.5 extend JWT token (also known as customization and enhancement)

  • Extend the JWT token by adding a Spring OAuth2 token enhancement class to the protected service;
//TokenEnhancer class needs to be extended
public class JWTTokenEnhancer implements TokenEnhancer {
    @Autowired
    private OrgUserRepository orgUserRepo;

    //Finds the user's organization ID based on the user name
    private String getOrgId(String userName){
        UserOrganization orgUser = orgUserRepo.findByUserName( userName );
        return orgUser.getOrganizationId();
    }

    //For this enhancement, you need to override the enhance() method
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        Map<String, Object> additionalInfo = new HashMap<>();
        String orgId =  getOrgId(authentication.getName());

        additionalInfo.put("organizationId", orgId);
        //All additional properties are placed in the HashMap and set on the accessToken variable passed into the method
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        return accessToken;
    }
}

3.6 tell the protected service to use JWTTokenEnhancer class

  • First, you need to expose the TokenEnhancer class;
@Bean
public TokenEnhancer jwtTokenEnhancer() {
    return new JWTTokenEnhancer();
}
  • Hook the TokenEnhancer class into the authentication service. Similar to 2.3 attaching JWT to authentication service;
@Configuration
public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private DefaultTokenServices tokenServices;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    //Auto assemble TokenEnhancer class
    @Autowired
    private TokenEnhancer jwtTokenEnhancer;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //Allow developers to hook multiple token enhancers
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));

        endpoints.tokenStore(tokenStore)                             //JWT
                .accessTokenConverter(jwtAccessTokenConverter)       //JWT
                .tokenEnhancer(tokenEnhancerChain)                   //JWT, hook the token enhancer chain to the endpoints parameter passed into the configure() method
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("eagleeye")
                .secret("thisissecret")
                .authorizedGrantTypes("refresh_token", "password", "client_credentials")
                .scopes("webclient", "mobileclient");
    }
}

3.7 use JJWT library to parse custom fields in JWT token in Zuul gateway

The following configurations are carried out in Zuul gateway service;

3.7.1 add POM in Zuul gateway XML dependency file

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.7.0</version>
</dependency>

3.7.2 add a new method to resolve serviceId

private String getOrganizationId(){
    String result="";
    if (filterUtils.getAuthToken()!=null){
        //Parse token from HTTP header Authorization
        String authToken = filterUtils.getAuthToken().replace("Bearer ","");
        try {
            //Pass in the signature key used to sign the token, and use the JWTS class to resolve the token
            Claims claims = Jwts.parser()
                    .setSigningKey(serviceConfig.getJwtSigningKey().getBytes("UTF-8"))
                    .parseClaimsJws(authToken).getBody();
            //Extract xxxId from token
            result = (String) claims.get("organizationId");
        }
        catch (Exception e){
            e.printStackTrace();
        }
    }
    return result;
}

last

Newcomer production, if there are mistakes, welcome to point out, thank you very much! Welcome to the official account and share some more everyday things. If you need to reprint, please mark the source!

Topics: Distribution Spring Cloud jwt microservice