Handson
1. Implement JWT Authentication for Employee application you have created in yesterday handson
Spring Security
- used to secure ur endpoints, who has privilege to access which endpoints
1. Add Spring security dependency (ie) spring-boot-starter-security, since we added security dependency by default security is enabled in ur appl
2. Start the appl, it will be provided with default password, run the appl http://localhost:5000/api/
- It will by default open login page provided by spring security
username: user
password: paste generated pwd
3. Inorder to use our own username, pwd, we have to configure username and pwd in application.properties
spring.security.user.name=Ram
spring.security.user.password=abcd
4. Start the appl, run http://localhost:5000/api/, it will by default open login page where we have to provide username and pwd configured in application.properties
But this approach is not recommended approach, since we have hardcoded only one username and pwd in application.properties
5. If we want to configure multiple username and pwd with different roles, then we have to create separate class with @Configuration and @EnableWebSecurity annotation where we provide the logic for authentication and authorization
Before Springboot3.0 version we have WebSecurityConfigureAdapter class which contains 2 overloaded configure() method
1. configure(AuthenticationManagerBuilder a) which used for authentication purpose (ie) validate username and pwd
2. configure(HttpSecurity h) which used for authorization (ie) we provide privilege to user so that which user can access which endpoint
But from Springboot3.0, this WebSecurityConfigureAdapter class is completely removed, then to perform authentication and authorization we have
1. UserDetailsService interface - used for authentication - 3 ways
a. In-memory authentication - we manually configure username and pwd along with their roles
b. Database authentication - fetch username and pwd from db
c. JWT Authentication
2. SecurityFilterChain interface - used for authorization
@Configuration
@EnableWebSecurity
public class SecurityConfig {
//Authentication
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
//In-Memory Authentication
UserDetails admin=User.withUsername("Ram")
.password(encoder.encode("abcd"))
.roles("ADMIN").build();
UserDetails user=User.withUsername("Sam")
.password(encoder.encode("xyz"))
.roles("USER").build();
return new InMemoryUserDetailsManager(admin,user);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//Authorization
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
return http.csrf(csrf->csrf.disable())
.authorizeHttpRequests(auth -> auth.requestMatchers("/api/welcome").permitAll()
.anyRequest().authenticated())
.formLogin().and().build();
}
}
6. Comment username and pwd in application.properties
7. Add mysql dependency in pom.xml
8. Start the appl and add movie info in table
http://localhost:5000/api/welcome - we can access without username and pwd
http://localhost:5000/api/movie - we can access only with username and pwd
http://localhost:5000/api/movie/100 - we can access only with username and pwd
9. Now both Ram and Sam can access all endpoints eventhough we have provided roles, now we want to restrict endpoints based on their roles for that we define @PreAuthorize annotation
@RestController
@RequestMapping("/api")
public class MovieController {
@Autowired
MovieService movieService;
@GetMapping("/welcome")
public String getMovieInfo() {
return "Welcome to Movie Application";
}
@PostMapping("/movie")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public ResponseEntity<Movie> createMovie(@Validated @RequestBody Movie movie) {
Movie savedMovie=movieService.createMovie(movie);
return new ResponseEntity<Movie>(savedMovie,HttpStatus.CREATED);
}
@GetMapping("/movie")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public ResponseEntity<List<Movie>> getAllMovies() {
List<Movie> movieList = movieService.getAllMovies();
if(movieList.isEmpty())
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
return new ResponseEntity<>(movieList,HttpStatus.CREATED);
}
@GetMapping("/movie/{id}")
@PreAuthorize("hasAuthority('ROLE_USER')")
public ResponseEntity<Movie> getMovieById(@PathVariable("id") Integer mid) {
Movie movie=movieService.getMovieById(mid);
return new ResponseEntity<>(movie,HttpStatus.CREATED);
}
}
- By defining @PreAuthorize we cannot achieve authorization, we have to inform that we have implemented method level authorization using @EnableMethodSecurity in SecurityConfig class
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
}
10. Start the appl
http://localhost:5000/api/welcome - everyone can access without username and pwd
http://localhost:5000/api/movie - only Ram can access with username and pwd
http://localhost:5000/api/movie/100 - only Sam can access with username and pwd
11. We have done SecurityConfig but we have hardcoded username and password so that we need in-memory authentication, but we need to fetch username and pwd from database
- Create entity class to store user info
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
@Id
private Integer id;
private String name;
private String email;
private String password;
private String role;
}
- Create repository interface
public interface UserInfoRepository extends JpaRepository<UserInfo, Integer>{
Optional<UserInfo> findByName(String name);
}
12. We need to perform authentication by fetching username and pwd from db, so we have to comment in-memory authentication and create separate class called UserInfoUserDetailsService to perform db authentication
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
//In-Memory Authentication
/*UserDetails admin=User.withUsername("Ram")
.password(encoder.encode("abcd"))
.roles("ADMIN","MANAGER").build();
UserDetails user=User.withUsername("Sam")
.password(encoder.encode("xyz"))
.roles("USER").build();
return new InMemoryUserDetailsManager(admin,user);*/
return new UserInfoUserDetailsService();
}
13. We create separate class called UserInfoUserDetailsService which implements UserDetailsService interface and override loadUserByUsername() which communicate with db and fetch username and pwd
@Component
public class UserInfoUserDetailsService implements UserDetailsService{
@Autowired
UserInfoRepository userRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<UserInfo> userInfo=userRepo.findByName(username);
return null;
}
}
- loadUserByUsername() method returns UserDetails object (ie) username, pwd, roles, but whatever we are returning from db are UserInfo object, so we have to convert UserInfo object to UserDetails object, so for that we have to create separate class called UserInfoUserDetails which implements UserDetails interface
public class UserInfoUserDetails implements UserDetails{
private String name;
private String password;
private List<GrantedAuthority> authorities;
public UserInfoUserDetails(UserInfo userInfo) {
name=userInfo.getName();
password=userInfo.getPassword();
authorities=Arrays.stream(userInfo.getRole().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// TODO Auto-generated method stub
return authorities;
}
@Override
public String getPassword() {
// TODO Auto-generated method stub
return password;
}
@Override
public String getUsername() {
// TODO Auto-generated method stub
return name;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- We have to implement this conversion of UserInfo to UserDetails in loadUserByUsername()
@Component
public class UserInfoUserDetailsService implements UserDetailsService{
@Autowired
UserInfoRepository userRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<UserInfo> userInfo=userRepo.findByName(username);
return userInfo.map(UserInfoUserDetails::new).orElseThrow(() -> new UsernameNotFoundException("User not found: "+username));
}
}
14. We create a separate endpoiny to store username and pwd in db, so in controller prg
@PostMapping("/new")
public String addNewUser(@RequestBody UserInfo userInfo) {
return movieService.addNewUser(userInfo);
}
- In service program,
@Autowired
UserInfoRepository userRepo;
@Autowired
PasswordEncoder encoder;
public String addNewUser(UserInfo userInfo) {
userInfo.setPassword(encoder.encode(userInfo.getPassword()));
userRepo.save(userInfo);
return "User added Successfully";
}
15. We need to provide authorization for "/new" and also for swagger, so in SecurityConfig
private static final String[] WHITE_LIST_URL = { "/api/v1/auth/**", "/v2/api-docs", "/v3/api-docs",
"/v3/api-docs/**", "/swagger-resources", "/swagger-resources/**", "/configuration/ui",
"/configuration/security", "/swagger-ui/**", "/webjars/**", "/swagger-ui.html", "/api/auth/**",
"/api/test/**", "/authenticate" };
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
return http.csrf(csrf->csrf.disable())
.authorizeHttpRequests(auth -> auth.requestMatchers("/api/welcome","/api/new").permitAll()
.requestMatchers(WHITE_LIST_URL).permitAll()
.anyRequest().authenticated())
.formLogin().and().build();
}
16. Start the appl, in swagger we add new user
{
"id": 1000,
"name": "Ram",
"email": ram@gmail.com,
"password": "abcd",
"role": "ROLE_ADMIN"
}
{
"id": 1001,
"name": "Sam",
"email": sam@gmail.com,
"password": "xyz",
"role": "ROLE_USER"
}
- Check database table
mysql> select * from user_info;
+------+---------------+------+--------------------------------------------------------------+------------+
| id | email | name | password | role |
+------+---------------+------+--------------------------------------------------------------+------------+
| 1000 | ram@gmail.com | Ram | $2a$10$YW0AeKjWujQzGefTwhjI6es0l65pEBothy344K5xihEMPT2Jdxh6a | ROLE_ADMIN |
| 1001 | sam@gmail.com | Sam | $2a$10$yu4oxhXE.KmfON6h4QETbuCTYsveJp10FoJgmwCN0TV41uGnMzSw2 | ROLE_USER |
+------+---------------+------+--------------------------------------------------------------+------------+
2 rows in set (0.00 sec)
17. We have defined UserDetailsService intf to communicate with db, we also need one more AuthenticationProvider bean to talk with UserDetailsService, so we have to configure DaoAuthenticationProvider in SecurityConfig
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider ap=new DaoAuthenticationProvider();
ap.setUserDetailsService(userDetailsService());
ap.setPasswordEncoder(passwordEncoder());
return ap;
}
18. Start the appl
http://localhost:5000/api/welcome - everyone can access without username and pwd
http://localhost:5000/api/movie - only Ram can access with username and pwd
http://localhost:5000/api/movie/100 - only Sam can access with username and pwd
JWT Authentication
Previously in order to access each and every endpoint we have to provide username and pwd in login screen, instead what we want to implement is we allow the user to give credential for the first time,and we generate one token for that username and going forward the user can pass that token to access any endpoint in the appl, this token is called JWT(Json Web Token)
JWT token has 3 parts
1. Header which contains algorithm which u want to generate the token and wht type of token
2. Payload contain claims which is nothing but subject(user), name for whom token is generated, when ur token is issued(iat), when ur token is expired(eat)
3. Verify Signature contains how we are generating the token
1. Add JWT dependency in pom.xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
2. Create DTO class called AuthRequest which contains username and pwd, based on this we are generating the token
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuthRequest {
private String username;
private String password;
}
3. Create endpoint in controller prg to generate the token based on username and pwd
@PostMapping("/authenticate")
public String generateToken(@RequestBody AuthRequest authRequest) {
}
4. Now we have AuthRequest and we want to give the username to JWT to generate the token, so we create separate class called JwtService class
@Component
public class JwtService {
public static final String SECRET="5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437";
//Generate the token based on username
public String generateToken(String username) {
Map<String,Object> claims=new HashMap<>();
return createToken(claims, username);
}
//create JWT token
private String createToken(Map<String, Object> claims, String username) {
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis()+1000*60*30)) //token is valid for 30min
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
private Key getSignInKey() {
byte[] keyBytes=Decoders.BASE64.decode(SECRET);
return Keys.hmacShaKeyFor(keyBytes);
}
}
5. Now in controller, we need to inject JwtService to generate the token
@Autowired
JwtService jwtService;
@PostMapping("/authenticate")
public String generateToken(@RequestBody AuthRequest authRequest) {
return jwtService.generateToken(authRequest.getUsername());
}
6. Now in SecurityConfig, we have to provide authorization for "/authenticate" endpoint
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
return http.csrf(csrf->csrf.disable())
.authorizeHttpRequests(auth -> auth.requestMatchers("/api/welcome","/api/new","/api/authenticate").permitAll()
.requestMatchers(WHITE_LIST_URL).permitAll()
.anyRequest().authenticated())
.formLogin().and().build();
}
7. Start the appl, in swagger we need to generate the token
{
"username": "Ram",
"password": "abcd"
}
It will generate the token for any users apart from user present in db or for invalid username and pwd
8. But we want to generate the token only for the users present inside db, so we need to add a restriction to the code so that only for users present inside db it has to generate the token
Now we have to authenticate the user before generating the token
- In SecurityConfig, we have to inject a bean called AuthenticationManager
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
- In "/authenticatee" endpoint, we have to authenticate user before generating token
@Autowired
AuthenticationManager am;
@PostMapping("/authenticate")
public String generateToken(@RequestBody AuthRequest authRequest) {
Authentication auth=am.authenticate(new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword()));
if(auth.isAuthenticated())
return jwtService.generateToken(authRequest.getUsername());
else
throw new UsernameNotFoundException("Invalid user request");
}
9. Start the appl,in swagger we need to generate the token
{
"username": "Ram",
"password": "abcd"
}
It will generate the token only for the correct users that are present in db
10. whenever client gives a request along with the token, we need to extract username from token and check with db whether that username exists or not, then only that user is validated so that we can access the endpoint
So we create separate class called JwtAuthFilter that extends OncePerRequestFilter and override doFilterInternal() method
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// TODO Auto-generated method stub
}
}
- When user send the request along with token, the token will be sending in header called "Authorization" as "Bearer token"
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Autowired
JwtService jwtService;
@Autowired
UserInfoUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//Extract token from Authorization header
String authHeader=request.getHeader("Authorization"); //Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJTYW0iLCJpYXQiOjE3NTk5OTM1OTUsImV4cCI6MTc1OTk5NTM5NX0.73Bj2GMFfUcbr9TycJtPVubaHI_QR45CUN0nkZ1BL6k
String token=null;
String username=null;
//check whether we have token or nor
if(authHeader != null && authHeader.startsWith("Bearer ")) {
token=authHeader.substring(7); //eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJTYW0iLCJpYXQiOjE3NTk5OTM1OTUsImV4cCI6MTc1OTk5NTM5NX0.73Bj2GMFfUcbr9TycJtPVubaHI_QR45CUN0nkZ1BL6k
//extract username from token
username=jwtService.extractUsername(token);
}
//Check username extracted from token is present in db or not
if(username !=null && SecurityContextHolder.getContext().getAuthentication()==null) {
UserDetails userDetails=userDetailsService.loadUserByUsername(username);
//validate JWT
if(jwtService.validateToken(token,userDetails)) {
UsernamePasswordAuthenticationToken authToken=new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
@Component
public class JwtService {
public static final String SECRET="5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437";
//Generate the token based on username
public String generateToken(String username) {
Map<String,Object> claims=new HashMap<>();
return createToken(claims, username);
}
//create JWT token
private String createToken(Map<String, Object> claims, String username) {
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis()+1000*60*30)) //token is valid for 30min
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
private Key getSignInKey() {
byte[] keyBytes=Decoders.BASE64.decode(SECRET);
return Keys.hmacShaKeyFor(keyBytes);
}
public String extractUsername(String token) {
return extractClaim(token,Claims::getSubject);
}
private <T> T extractClaim(String token, Function<Claims,T> claimResolver) {
final Claims claims=extractAllClaims(token);
return claimResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
public boolean validateToken(String token, UserDetails userDetails) {
final String username=extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token,Claims::getExpiration);
}
}
11. Now we tell SecurityConfig to use JwtAuthFilter which we have created before using any predefined filter, so in SecurityConfig we have to enable Session and authFilter
@Autowired
JwtAuthFilter authFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
return http.csrf(csrf->csrf.disable())
.authorizeHttpRequests(auth -> auth.requestMatchers("/api/welcome","/api/new","/api/authenticate").permitAll()
.requestMatchers(WHITE_LIST_URL).permitAll()
.anyRequest().authenticated())
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider())
.addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
12. To make swagger to understand JWT token authentication and provide JWT token, we have to create separate class called SwaggerConfig
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenApi() {
return new OpenAPI().info(new Info().title("Movie Application with JWT Authentication"))
.addSecurityItem(new SecurityRequirement().addList("SecurityScheme"))
.components(new Components().addSecuritySchemes("SecurityScheme", new SecurityScheme().name("SecurityScheme").type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("jwt")));
}
}
13. Start the appl
No comments:
Post a Comment