Using Spring Boot for OAuth2 and JWT REST API Security

Hallo guys, pada tutorial ini, saya akan mempraktikan secara langsung membuat aplikasi Spring Boot yang menggunakan otentikasi JWT untuk mengamankan API REST yang terbuka. Dalam contoh ini, saya akan menggunakan user hard-coded untuk otentikasi, maksudnya yaitu saat ini belum menggunakan database. Setiap user akan dapat menggunakan API ini hanya jika ia memiliki JSON Web Token (JWT) yang valid. Untuk ilustrasi nya bisa kalian lihat pada gambar berikut ini. 

Dalam tutorial ini saya tidak menjelaskan secara mendetail tentang JWT anda dapat membaca dan memahaminya sebelum melakukan praktik ini. Namun saya hanya sedikit menjelaskan tentang beberapa pengertian dari Authorization server, Resource Server, OAuth2 dan Token JWT.

Authorization server
Authorization server adalah Server yang mengeluarkan token akses setelah berhasil mengotentikasi a clientcdan resource owner, dan mengesahkan sebuah permintaan ,  authorization juga merupakan komponen arsitektur tertinggi untuk Keamanan API Web. 

Resource Server
Resource Server adalah Server yang menangani permintaan yang diautentikasi setelah client memperoleh access token, atau aplikasi yang menyediakan token akses ke klien untuk mengakses Endpoint HTTP.

OAuth2
OAuth2 adalah kerangka kerja authorization yang memungkinkan aplikasi Keamanan Web untuk mengakses sumber daya dari klien.

Token JWT
JWT Token adalah JSON Web Token, yang digunakan untuk mewakili pihak yang diklaim yang diamankan antara dua pihak. Anda dapat mempelajari lebih lanjut tentang token JWT di www.jwt.io/

Untuk pemahaman yang lebih baik sedikit rangkuman tentang apa saja yang akan kita lakukan di tutorial ini :

1. mendevelop aplikasi Spring Boot yang dengan  REST GET API dengan perintah /hello.

2. Konfigurasikan Spring Security untuk JWT.  Membuka REST POST API dengan    mapping/otentikasi menggunakan Pengguna mana yang akan mendapatkan Token Web JSON yang   valid. Dan kemudian, izinkan usermengakses API /hello hanya jika memiliki token yang valid.

Baikilah langsung saja sekarang kita akan membangun aplikasi OAuth2 yang memungkinkan penggunaan Resource Server dan Authorization server dengan bantuan Token JWT.

Berikut struktur project yang akan kita buat.


Buat file project java spring baru di laptop kalian.
Kemudian coba buat sebuah class controller
Controller.java
package com.enigma.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Controller {
    @RequestMapping({ "/hello" })
    public String firstPage() {
        return "Hello World";
    }
}

Tambahan port server di application.properties.
server.port=8080

Kemudian coba jalankan project lewat postman atau lewat browser : localhost:8080/hello
SPRING SECURITY DAN JWT CONFIGURATION 
membuat JWT - membuka POST API dengan mapping/otentikasi . Ketika melewati nama pengguna dan kata sandi yang benar, itu akan menghasilkan JSON Web Token (JWT).

Memvalidasi JWT - Jika pengguna mencoba mengakses GET API dengan mapping /hello . Ini akan memungkinkan akses hanya jika permintaan memiliki Token Web JSON (JWT) yang valid

Tambahkan depedency di project maven.
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

Tambahkan code ini di application.properties
jwt.secret=enigma

JwtTokenUtil
JwtTokenUtil bertanggung jawab untuk melakukan operasi JWT seperti pembuatan dan validasi. Ini menggunakan io.jsonwebtoken.Jwts untuk mencapai ini.

Buat package config dan buat file JwtTokenUtil.java
package com.enigma.config;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@Component
public class JwtTokenUtil implements Serializable {
    private static final long serialVersionUID = -2550185165626007488L;
    public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
    @Value("${jwt.secret}")
    private String secret;
    //retrieve username from jwt token
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }
    //retrieve expiration date from jwt token
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }
    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }
    //for retrieveing any information from token we will need the secret key
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }
    //check if the token has expired
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }
    //generate token for user
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername());
    }
    //while creating the token -
    //1. Define  claims of the token, like Issuer, Expiration, Subject, and the ID
    //2. Sign the JWT using the HS512 algorithm and secret key.
    //3. According to JWS Compact Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
    //   compaction of the JWT to a URL-safe string
    private String doGenerateToken(Map<String, Object> claims, String subject) {
        return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
                .signWith(SignatureAlgorithm.HS512, secret).compact();
    }
    //validate token
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

JwtUserDetailsSevice

Jwt userdetailservice digunakan untuk mengambil data username dan password dari database namun pada praktik kali ini masih belum menggunakan database , untuk tutorial selanjutnya kita akan mennggunakan database untuk mengambil data user dan password

Buat package service dan buat file JwtUserDetailsService.java
Jngan lupa Implements UserDetailsService dari spring security

Info :     username = javainuse 
              password = password
package com.enigma.service;
import java.util.ArrayList;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class JwtUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if ("javainuse".equals(username)) {
            return new User("javainuse", "$2a$10$slYQmyNdGzTn7ZLBXBChFOC9f6kFjAqPhccnP6DxlWXx2lPk1C3G6",
                    new ArrayList<>());
        } else {
            throw new UsernameNotFoundException("User not found with username: " + username);
        }
    }
}

JwtAuthenticationController

membuka  POST API /otentikasi menggunakan JwtAuthenticationController. API POST berfungsi mendapatkan username dan password dalam body- Menggunakan Spring Authentication Manager kami mengotentikasi username dan password. Jika credintial valid, token JWT dibuat menggunakan JWTTokenUtil dan diberikan kepada client.


Buat package Controller dan buat file baru JwtAuthenticationController.java
package com.enigma.controller;
import java.util.Objects;

import com.enigma.config.JwtTokenUtil;
import com.enigma.model.JwtRequest;
import com.enigma.model.JwtResponse;
import com.enigma.service.JwtUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@CrossOrigin
public class JwtAuthenticationController {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private JwtUserDetailsService userDetailsService;
    @RequestMapping(value = "/authenticate", method = RequestMethod.POST)
    public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) throws Exception {
        authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
        final UserDetails userDetails = userDetailsService
                .loadUserByUsername(authenticationRequest.getUsername());
        final String token = jwtTokenUtil.generateToken(userDetails);
        return ResponseEntity.ok(new JwtResponse(token));
    }
    private void authenticate(String username, String password) throws Exception {
        try {
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        } catch (DisabledException e) {
            throw new Exception("USER_DISABLED", e);
        } catch (BadCredentialsException e) {
            throw new Exception("INVALID_CREDENTIALS", e);
        }
    }
}

JwtRequest
class ini diperlukan untuk menyimpan username dan password yang diterima dari client.

Buat package model dan buat file baru JwtRequest.java
package com.enigma.model;
import java.io.Serializable;
public class JwtRequest implements Serializable {
    private static final long serialVersionUID = 5926468583005150707L;

    private String username;
    private String password;

    //need default constructor for JSON Parsing
    public JwtRequest()
    {

    }
    public JwtRequest(String username, String password) {
        this.setUsername(username);
        this.setPassword(password);
    }
    public String getUsername() {
        return this.username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return this.password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

JwtResponse
class ini diperlukan untuk membuat respons yang berisi JWT untuk dikembalikan kepada user.

Buat file baru JwtResponse.java di package model
package com.enigma.model;
import java.io.Serializable;
public class JwtResponse implements Serializable {
    private static final long serialVersionUID = -8091879091924046844L;
    private final String jwttoken;
    public JwtResponse(String jwttoken) {
        this.jwttoken = jwttoken;
    }
    public String getToken() {
        return this.jwttoken;
    }
}

JwtRequestFilter

Untuk setiap permintaan masuk, kelas Filter ini dijalankan. Ia memeriksa apakah permintaan memiliki token JWT yang valid. Jika memiliki Token JWT yang valid maka ia menetapkan Otentikasi dalam konteks, untuk menentukan bahwa pengguna saat ini diautentikasi.

Buat package config dan buat file baru bernama JwtRequestFilter.java

package com.enigma.config;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.enigma.service.JwtUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.jsonwebtoken.ExpiredJwtException;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUserDetailsService jwtUserDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        final String requestTokenHeader = request.getHeader("Authorization");
        String username = null;
        String jwtToken = null;
        // JWT Token is in the form "Bearer token". Remove Bearer word and get
        // only the Token
        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7);
            try {
                username = jwtTokenUtil.getUsernameFromToken(jwtToken);
            } catch (IllegalArgumentException e) {
                System.out.println("Unable to get JWT Token");
            } catch (ExpiredJwtException e) {
                System.out.println("JWT Token has expired");
            }
        } else {
            logger.warn("JWT Token does not begin with Bearer String");
        }
        // Once we get the token validate it.
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
            // if token is valid configure Spring Security to manually set
            // authentication
            if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                usernamePasswordAuthenticationToken
                        .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                // After setting the Authentication in the context, we specify
                // that the current user is authenticated. So it passes the
                // Spring Security Configurations successfully.
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        chain.doFilter(request, response);
    }
}

JwtAuthenticationEntryPoint
Class ini akan menolak setiap permintaan yang tidak diautentikasi dan mengirim kode kesalahan 401

Buat file JwtAuthenticationEntryPoint.java di package config
package com.enigma.config;
import java.io.IOException;
import java.io.Serializable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
    private static final long serialVersionUID = -7858869558953243875L;
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

WebSecurityConfig
Buat file WebSecurityConfig.java di package config
package com.enigma.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    @Autowired
    private UserDetailsService jwtUserDetailsService;
    @Autowired
    private JwtRequestFilter jwtRequestFilter;
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        // configure AuthenticationManager so that it knows from where to load
        // user for matching credentials
        // Use BCryptPasswordEncoder
        auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        // We don't need CSRF for this example
        httpSecurity.csrf().disable()
                // dont authenticate this particular request
                .authorizeRequests().antMatchers("/authenticate").permitAll().
                // all other requests need to be authenticated
                        anyRequest().authenticated().and().
                // make sure we use stateless session; session won't be used to
                // store user's state.
                        exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // Add a filter to validate the tokens with every request
        httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

Untuk memulai project Spring Aplication :
  • Generate Json WebToken
  • Jalankan localhost:8080/authenticate kemudian masukan body : username dan password ,
  • Maka akan return sebuah token
Kemudian validation token di header untuk mengakses localhost:8080/hello


Selamat mencoba ^-^


0 Response to "Using Spring Boot for OAuth2 and JWT REST API Security"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel