110

Microservices Security using JWT Authentication Gateway

 5 years ago
source link: https://www.tuicool.com/articles/hit/bUbQf2A
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Introduction

This blog provides a deep dive on the use of an Authentication Gateway for providing secured access to Microservices. It describes how the Gateway uses JSON Web Token(JWT) for authenticating clients that want to access web service endpoints hosted by different Microservices.

JSON Web Token (JWT)

As per RFC 7519 , JWT is a compact and self-contained way for secure transmission of information between different entities as a JSON object. The token is composed of 3 parts: header, payload and signature; each separated by a dot as mentioned in below format:

header.payload.signature

Header

The header typically consists of two parts:

  1. Type of Token i.e. JWT
  2. Hashing algorithm being used e.g. HMAC SHA256 or RSA

e.g.

<em>{</em>
<em>  "alg": "HS256",</em>
<em>  "typ": "JWT"</em>
<em>}</em>

The first part of the JWT is formed by Base64Url encoding of the Header JSON.

  Payload

The payload contains the ‘claims’ i.e. the data that‘s stored inside the JWT. Claims are information about an entity (typically, the user) and additional data.

e.g.

<em>{</em>
<em> "sub": "1234567890",</em>
<em> "name": "Mark",</em>
<em> "admin": true,</em>
<em> "iss": http://abc.com,</em>
<em> "iat": 1472033308,</em>
<em> "exp": 1472034208</em>
<em>}</em>

The second part of the JWT is formed by Base64Url encoding of the payload.

  Signature

The signature is created by signing the encoded header & encoded payload with a secret key using the algorithm specified in the header.

e.g. For HMAC SHA256 algorithm, the signature will be created as below:

<em>HMACSHA256(</em><em>  
base64UrlEncode(header) + "." +
</em><em>base64UrlEncode(payload),  secret)
</em>

After combining all JWT components together, a typical JWT token looks like below.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjQifQ.
-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM

The JWT token should be sent in the Authorization header using the Bearer schema for accessing a protected resource as shown below:

<em>Authorization: Bearer <JWT token>

</em>

JWT Advantages

  1. JWT uses JSON which is less verbose than XML & therefore smaller in size making it more compact than Security Assertion Markup Language Tokens (SAML)
  2. It is easier to work with JWT than SAML assertions as JSON parsers are common in most programming languages but XML doesn’t have a natural document-to-object mapping.

System Architecture

The system is implemented as a bunch of Spring Boot applications communicating with each other. Apache Maven is used as a dependency & build tool for the applications. The system components are best understood from the below workflow diagram.

ueQNjmR.png!web

System Components & description:

  1. Client

It can be any Web service consumer on any platform. Simply put it can be another Webservice, UI application or Mobile platform which wants to read-write data in a secure way with an Application having various Microservices. Typically, the Client requests shown in the above workflow diagram can be REST API requests invoked from any REST client.

  1. Eureka Service Registry

The Eureka service registry is another Microservice that primarily handles the communication of different Microservices. As each Microservice registers itself in the Eureka server, it must be highly available since every service communicates with it to discover other services. Below is the configuration for a typical Eureka server.

pom.xml

<em><dependencies></em>
<em>   <dependency></em>
<em>      <groupId>org.springframework.cloud</groupId></em>
<em>      <artifactId>spring-cloud-starter-eureka-server</artifactId></em>
<em>   </dependency></em>
<em></dependencies>
</em>

application.yml

<em>server:</em>
<em>  port: ${PORT:8761}</em>
<em> </em><em>eureka:</em>
<em>  instance:</em>
<em>    hostname: localhost</em>
<em>  client:</em>
<em>    registerWithEureka: false</em>
<em>    fetchRegistry: false</em>
<em>    serviceUrl:</em>
<em>      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/</em>
<em>@SpringBootApplication</em>
<em>@EnableEurekaServer</em>
<strong><em>public</em></strong> <strong><em>class</em></strong><em> EurekaServerApplication {</em>
<em>    </em><strong><em>public</em></strong> <strong><em>static</em></strong> <strong><em>void</em></strong><em> main(String[] args) {</em>
<em>        SpringApplication.run(EurekaServerApplication.class, args);</em>
<em>    }</em>
<em>}</em>

Navigating the browser to http://localhost:8761 shows the Eureka dashboard, where one can inspect the registered Microservice instances & other status information.

3.MicroServices A & B

These are the Microservices that host various REST endpoints namely GET/POST/PUT/DELETE for different resources of the application. Each Microservice is a Eureka client application & must register itself with the Eureka Server. Below is the configuration for MicroserviceA that is hosted on port 8082 with some REST endpoints.

pom.xml

<em><dependencies></em>
<em>   <dependency></em>
<em>      <groupId>org.springframework.boot</groupId></em>
<em>      <artifactId>spring-boot-starter-web</artifactId></em>
<em>   </dependency></em>
<em>   <dependency></em>
<em>      <groupId>org.springframework.cloud</groupId></em>
<em>      <artifactId>spring-cloud-starter-eureka</artifactId></em>
<em>   </dependency></em>
<em></dependencies></em>

application.yml

<em>server:</em>
<em>  port: ${PORT:8082}</em>
<em>spring:  </em>
<em>  application:</em>
<em>    name: greeting-service</em>
<em>eureka:</em>
<em>  client:</em>
<em>    fetchRegistry: true</em>
<em>    serviceUrl:</em>
<em>      defaultZone: http://localhost:8761/eureka/</em>
<em>@SpringBootApplication</em>
<em>@EnableDiscoveryClient
@RestController</em>
<em>@RequestMapping("/greetings")</em>
<strong><em>public</em></strong> <strong><em>class</em></strong><em> MicroServiceA {</em>
<em>    </em><em>@GetMapping</em>
<em>    </em><strong><em>public</em></strong><em> String fetchGreeting() {</em>
<em>        </em><strong><em>return</em></strong> <em>"Hello from MicroServiceA"</em><em>;</em>
<em>    }</em>
<em>    @PostMapping</em>
<em>    </em><strong><em>public</em></strong><em> String addGreeting(@RequestBody String greeting) {</em>
<em>         // Business logic to save the greeting typically to a DB table</em>
<strong><em>         return</em></strong> <em>"Greeting successfully saved"</em><em>;</em>
<em>    }</em>
<em>    </em><strong><em>public</em></strong> <strong><em>static</em></strong> <strong><em>void</em></strong><em> main(String[] args) {</em>
<em>        SpringApplication.run(MicroServiceA.class, args);</em>
<em>    }</em>
<em>}</em>

On similar lines, MicroserviceB can be configured on another port to host other REST endpoints.

4.Authentication Gateway

The Gateway is implemented as a Microservice using Spring Cloud Zuul Proxy & Spring Security APIs. It handles centralized authentication & routing client requests to various Microservices using the Eureka service registry. It acts as a proxy to the clients abstracting the Microservices architecture & must be highly available as it works as a single point of interaction for different operations be it user signup, user credentials authentication, generating & verifying JWT tokens & handling the client business requests by communicating to relevant Microservice endpoints. In short, it is a Request router that doubles up as an Authentication Microservice.

Alternatively, the User management features including different authentication mechanisms (JWT, OAuth) can also be hosted as a separate ‘User Management’ Microservice. In that case, the Gateway can just work as a lightweight request router & communicate with it for user authentication via the JWT tokens.

Below is the configuration for the Authentication Gateway.

pom.xml

   <em><dependency></em>
<em>         <groupId>org.springframework.cloud</groupId></em>
<em>         <artifactId>spring-cloud-starter-zuul</artifactId></em>
<em>   </dependency></em>
<em>   <dependency></em>
<em>         <groupId>org.springframework.cloud</groupId></em>
<em>         <artifactId>spring-cloud-starter-eureka</artifactId></em>
<em>   </dependency></em>
<em>   <dependency></em>
<em>        <groupId>org.springframework.boot</groupId></em>
<em>        <artifactId>spring-boot-starter-security</artifactId></em>
<em>   </dependency>         </em>
<em>   <dependency></em>
<em>         <groupId>org.springframework.boot</groupId></em>
<em>         <artifactId>spring-boot-starter-web</artifactId></em>
<em>   </dependency></em>
<em>   <dependency></em>
<em>         <groupId>io.jsonwebtoken</groupId></em>
<em>         <artifactId>jjwt</artifactId></em>
<em>         <version>0.6.0</version></em>
<em>   </dependency></em>
<em>   <!—- Other dependencies -- >  </em>

application.yml

<em>server:</em>
<em>  port: ${PORT:8081}</em>
<em>eureka:</em>
<em>  client:</em>
<em>    fetchRegistry: true</em>
<em>    serviceUrl:</em>
<em>      defaultZone: </em><em><a href="http://localhost:8761/eureka/">http://localhost:8761/eureka/</a></em>
<em>zuul:</em>
<em>  routes:</em>
<em>    serviceA:</em>
<em>      path: /greetings-api/**</em>
<em>      serviceId: greeting-service</em>
<em>    serviceB:</em>
<em>      path: /tasks-api/**</em>
<em>      serviceId: task-service</em>

application.properties

<em>jwt.security.key=j3H5Ld5nYmGWyULy6xwpOgfSH++NgKXnJMq20vpfd+8=t</em>

The JWT secret key is used during a signing of the JWT token

<em>@SpringBootApplication</em>
<em>@EnableDiscoveryClient</em>
<em>@EnableZuulProxy</em>
<strong><em>public</em></strong> <strong><em>class</em></strong><em> ApiGatewayApplication{</em>
<em>    </em><strong><em>public</em></strong> <strong><em>static</em></strong> <strong><em>void</em></strong><em> main(String[] args) {</em>
<em>        SpringApplication.run(ApiGatewayApplication.class, args);</em>
<em>    }</em>
<em>    @Bean</em>
<em>    public BCryptPasswordEncoder passwordEncoder() {</em>
<em>        return new BCryptPasswordEncoder(); // For encrypting user password</em>
<em>    }</em>
<em>}</em>

JWT Authentication Workflow

  1. Client registers with Authentication Gateway by supplying the username & password through the POST URI /users/signup (which is permitted for public access without any security)

Web security configuration

<em>@Configuration</em>
<em>@EnableWebSecurity</em>
<em>@EnableGlobalMethodSecurity(prePostEnabled = true)</em>
<em>public class WebSecurityConfig extends WebSecurityConfigurerAdapter {</em>
<em>    @Resource(name = "userService")</em>
<em>    private UserDetailsService userDetailsService;</em>
<em>    @Autowired</em>
<em>    private JwtAuthenticationEntryPoint unauthorizedHandler;</em>
<em>    @Autowired</em>
<em>    private BCryptPasswordEncoder passwordEncoder;</em>
<em>    @Override</em>
<em>    @Bean</em>
<em>    public AuthenticationManager authenticationManagerBean() throws Exception{      </em>
<em>        return super.authenticationManagerBean();</em>
<em>    }</em>
<em>    @Autowired</em>
<em>    public void globalUserDetails(AuthenticationManagerBuilder auth) throws   </em>
<em>             Exception {</em>
<em>        auth.userDetailsService(userDetailsService)</em>
<em>                .passwordEncoder(passwordEncoder);</em>
<em>    }</em>
<em>    @Bean</em>
<em>    public JwtAuthenticationFilter authenticationTokenFilterBean() throws </em>
<em>              Exception {</em>
<em>        return new JwtAuthenticationFilter();</em>
<em>    }</em>
<em>    @Override</em>
<em>    protected void configure(HttpSecurity http) throws Exception {</em>
<em>        http.cors().and().csrf().disable()                                       </em>
<em>            .authorizeRequests()</em>
<em>            .antMatchers("/token/*","/users/signup").permitAll()</em>
<em>            .anyRequest().authenticated()</em>
<em>            .and()</em>
<em>            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)</em>
<em>            .and()</em>
<em>            .sessionManagement()</em>
<em>            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);</em>
<em>       http.addFilterBefore(authenticationTokenFilterBean(),
   UsernamePasswordAuthenticationFilter.class);</em>
<em>    }</em>
<em>}</em>

UserController

<em>@RestController</em>
<em>@RequestMapping("/users")</em>
<em>public class UserController {</em>
<em>    @Autowired</em>
<em>    private UserService userService;</em>
<em>    @PostMapping("/signup")</em>
<em>    public User saveUser(@RequestBody LoginUser user){</em>
<em>      return userService.save(user);</em>
<em>    }</em>
<em>    // Other methods</em>
<em>}</em>

Here UserService is an implementation of Spring Security’s UserDetailsService & the password is encrypted using BCryptPasswordEncoder before storing it into the DB.

<em>@Service(value = "userService")</em>
<em>public class UserServiceImpl implements UserDetailsService, UserService {</em>
<em>   @Autowired</em>
<em>   private UserRepository userDao;</em><em>   </em>
<em>   @Autowired</em>
<em>   private BCryptPasswordEncoder passwordEncoder;</em><em>      </em>
<em>   @Override</em>
<em>   public User save(LoginUser user) {</em>
<em>         User newUser = new User();</em>
<em>         newUser.setUsername(user.getUsername());</em>
<em>         newUser.setPassword(passwordEncoder.encode(user.getPassword()));</em>
<em>         return userDao.save(newUser);</em>
<em>    }</em>
<em>   public UserDetails loadUserByUsername(String userId) throws</em>
<em>               UsernameNotFoundException {</em>
<em>         User user = userDao.findByUsername(userId);</em>
<em>         if(user == null){</em>
<em>            throw new UsernameNotFoundException("Invalid username or password.");</em>
<em>         }</em>
<em>         return new org.springframework.security.core.userdetails.User(</em>
<em>            user.getUsername(), user.getPassword(), getAuthority());</em>
<em>         }</em>
<em>  // Other service methods
</em>

2.Client requests an ‘Access token’ from Authentication Gateway through the POST URI /token/generate-token by sending their credentials.

3.The Authentication Gateway verifies the credentials & upon successful authentication generates a JWT access token containing user details and permissions. This token is sent as a response to the client.

AuthenticationController

<em>@RestController</em>
<em>@RequestMapping("/token")</em>
<em>public class AuthenticationController {</em>
<em>    @Autowired</em>
<em>    private AuthenticationManager authenticationManager;</em>
<em>    @Autowired</em>
<em>    private JwtTokenUtil jwtTokenUtil;</em>
<em>    @Autowired</em>
<em>    private UserService userService;</em>
<em>    @RequestMapping(value = "/generate-token", method = RequestMethod.POST)</em>
<em>    public ResponseEntity<?> generateToken(@RequestBody LoginUser loginUser) </em>
<em>              throws AuthenticationException {</em>
<em>        final Authentication authentication = authenticationManager.authenticate(</em>
<em>                new UsernamePasswordAuthenticationToken(</em>
<em>                        loginUser.getUsername(),</em>
<em>                        loginUser.getPassword()</em>
<em>                )</em>
<em>        );</em>
<em>        SecurityContextHolder.getContext().setAuthentication(authentication);</em>
<em>        final User user = userService.findOne(loginUser.getUsername());</em>
<em>        final String token = jwtTokenUtil.generateToken(user);</em>
<em>        return ResponseEntity.ok(new AuthToken(token));</em>
<em>    }</em>
<em>}</em>

JwtTokenUtil

<em>@Component</em>
<em>public class JwtTokenUtil implements Serializable {</em><em>    </em>
<em>    @Value("${jwt.security.key}")</em>
<em>    private String jwtKey;</em>
<em>    private String doGenerateToken(String subject) {</em>
<em>        Claims claims = Jwts.claims().setSubject(subject);</em>
<em>        return Jwts.builder()</em>
<em>                .setClaims(claims)</em>
<em>                .setIssuer("http://jwtdemo.com")</em>
<em>                .setIssuedAt(new Date(System.currentTimeMillis()))</em>
<em>                .setExpiration(new Date(System.currentTimeMillis() +           </em>
<em>                          ACCESS_TOKEN_VALIDITY_SECONDS*1000))</em>
<em>                .signWith(SignatureAlgorithm.HS256, jwtKey)</em>
<em>                .compact();</em>
<em>    }</em>
<em>   // Other methods</em>
<em>}</em>

Here client will invoke below POST request to obtain an access token having a validity of configured time (few minutes to few hours):

http://localhost:8081/token/generate-token

Sample Request Body:

{    
    "username": "admin",
    "password": "password"
}

JWT access token returned by Authentication Gateway

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInNjb3BlcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0seyJhd
XRob3JpdHkiOiJST0xFX0FETUlOIn1dLCJpc3MiOiJodHRwOi8vand0ZGVtby5jb20iLCJpYXQiOjE1MTg3NjM0N
TUsImV4cCI6MTUxODc4MTQ1NX0.t8UUBrhYx6lUAunl5R-s17IxZXOZ1yYGLwV0Sdiw4QY

 4. Client then sends the Access token in an Authorization header in each REST API request to the Authentication Gateway.

e.g. To access the GET URI ‘greetings’ using below details:

URL: http://localhost:8081/greetings-api/greetings

Authorization Header: Bearer

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInNjb3BlcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0seyJhd
XRob3JpdHkiOiJST0xFX0FETUlOIn1dLCJpc3MiOiJodHRwOi8vand0ZGVtby5jb20iLCJpYXQiOjE1MTg3NjM0N
TUsImV4cCI6MTUxODc4MTQ1NX0.t8UUBrhYx6lUAunl5R-s17IxZXOZ1yYGLwV0Sdiw4QY

5.Authentication Gateway retrieves the access token from Authorization header in the client request and validates the signature. If the signature is valid it routes the request to the matching endpoint (Microservice) based upon the routes which are configured in application.yml or application.properties. Microservice level authorization can also be handled through the JwtAuthenticationFilter.

e.g. All endpoints of MicroserviceA can be accessed by users having ADMIN role only

JwtAuthenticationFilter

<em>@public class JwtAuthenticationFilter extends OncePerRequestFilter {</em>
<em>    @Autowired</em>
<em>    private UserDetailsService userDetailsService;</em>
<em>    @Autowired</em>
<em>    private JwtTokenUtil jwtTokenUtil;</em>
<em>    @Override</em>
<em>    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse </em>
<em>                 res, FilterChain chain) throws IOException, ServletException {</em>
<em>        String header = req.getHeader(HEADER_STRING);</em>
<em>        String username = null;</em>
<em>        String authToken = null;</em>
<em>        if (header != null && header.startsWith(TOKEN_PREFIX)) {</em>
<em>            authToken = header.replace(TOKEN_PREFIX,"");</em>
<em>            try {</em>
<em>                username = jwtTokenUtil.getUsernameFromToken(authToken);</em>
<em>            } catch (IllegalArgumentException e) {</em>
<em>                logger.error("An error occured during getting username from </em>
<em>                          token", e);</em>
<em>            } catch (ExpiredJwtException e) {</em>
<em>                logger.warn("Token is expired and not valid anymore", e);</em>
<em>            } catch(SignatureException e){</em>
<em>                logger.error("Authentication Failed. Username or Password not </em>
<em>                          valid.");</em>
<em>            }</em>
<em>        } else {</em>
<em>            logger.warn("Couldn't find bearer string, will ignore the header");</em>
<em>        }</em>
<em>        if (username != null && </em>
<em>                 SecurityContextHolder.getContext().getAuthentication() == null) {</em>
<em>            UserDetails userDetails = </em>
<em>                          userDetailsService.loadUserByUsername(username);</em>
<em>            if (jwtTokenUtil.validateToken(authToken, userDetails)) {</em>
<em>                UsernamePasswordAuthenticationToken authentication = new </em>
<em>                     UsernamePasswordAuthenticationToken(userDetails, null, null);</em>
<em>                authentication.setDetails(new </em>
<em>                     WebAuthenticationDetailsSource().buildDetails(req));</em>
<em>                logger.info("Authenticated user " + username + ", setting security </em>
<em>                          context");                          </em>
<em>                SecurityContextHolder.getContext().setAuthentication(</em>
<em>                          authentication);</em>
<em>            }</em>
<em>        }</em>
<em>        chain.doFilter(req, res);</em>
<em>    }</em>
<em>}</em>

6.The gateway can also send extra parameters in the request header (JWT token, other user information etc.) through custom Zuul Filters.

PreFilter

<em>public class PreFilter extends ZuulFilter {</em>
<em>    private static Logger log = LoggerFactory.getLogger(PreFilter.class);</em>
<em>    @Override</em>
<em>    public String filterType() {</em>
<em>        return "pre";</em>
<em>    }</em>
<em>    @Override</em>
<em>    public int filterOrder() {</em>
<em>        return 1;</em>
<em>    }</em>
<em>    @Override</em>
<em>    public boolean shouldFilter() {</em>
<em>        return true;</em>
<em>    }</em>
<em>    @Override</em>
<em>    public Object run() {</em>
<em>        RequestContext ctx = RequestContext.getCurrentContext();</em>
<em>        HttpServletRequest request = ctx.getRequest();</em><em>    </em>
<em>         // Add a custom header in the request</em>
<em>        ctx.addZuulRequestHeader("Authorization",</em>
<em>                 request.getHeader("Authorization"));</em>
<em>        log.info(String.format("%s request to %s", request.getMethod(), </em>
<em>                 request.getRequestURL().toString()));</em>
<em>        return null;</em>
<em>    }</em>
<em>}</em>

7.The Micro service then can optionally authorize the request & provides the response to the Authentication Gateway.

For Authorization, the Microservice would need the JWT access token to be passed to it. It can then verify the JWT token & extract the user roles from the claims & accordingly allow/deny the request for the concerned endpoint.

e.g. For authorizing only users with ADMIN role to access the REST endpoint for ‘addGreeting’ in MicroServiceA, it can be annotated as below.

    <em>@PostMapping</em>
<em>    @PreAuthorize("hasRole('ROLE_ADMIN')")</em>
<em>    </em><strong><em>public</em></strong><em> String addGreeting(@RequestBody String greeting) {</em>
<em>         // Business logic to save the greeting typically to a DB table</em>
<strong><em>         return</em></strong> <em>"Greeting successfully saved"</em><em>;</em>
<em>    }
</em>

8.The Gateway then can optionally add any other header to the response using a ‘post’ Zuul filter & send it back to the client.

Conclusion

JWT Authentication Gateway provides very a useful approach for securing Microservices applications with minimal impact to the Microservices code. Thus, application developers can focus on the core business logic without worrying about the security mechanism that guards the application. It can be independently scaled and deployed for performing load testing & maintaining high availability. It can also serve as a centralized entity for other cross-cutting concerns like Microservices monitoring, routing rules etc.

Further Reading and Next Steps:

  1. Provide a mechanism of fault tolerance for Microservices using Hystrix
  2. Prevent the ‘User Signup’ URL from abuse i.e. prevent attackers from misusing it or provide a limit on the number of signups per minute/hour etc.
  3. Provide endpoints for ‘Password Reset’ & ‘Forgot Password’ to the client
  4. JWT Refresh tokens
  5. Provide API documentation for each Microservice using Swagger

References:

  1. https://jwt.io/introduction/
  2. https://dzone.com/articles/microservices-communication-zuul-api-gateway-1
  3. https://spring.io/guides/gs/routing-and-filtering/

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK