Browse Source

JWT implementation

Daniel Garcia Costa 1 year ago
parent
commit
eddff2c5aa

+ 13 - 0
src/main/java/es/uv/garcosda/config/WebConfigSecurity.java

@@ -1,5 +1,6 @@
 package es.uv.garcosda.config;
 
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.HttpStatus;
@@ -10,6 +11,8 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.server.SecurityWebFilterChain;
 
+import es.uv.garcosda.security.AuthenticationManager;
+import es.uv.garcosda.security.SecurityContextRepository;
 import reactor.core.publisher.Mono;
 
 @EnableReactiveMethodSecurity
@@ -17,6 +20,12 @@ import reactor.core.publisher.Mono;
 @Configuration
 public class WebConfigSecurity {
 
+	@Autowired
+    private AuthenticationManager authenticationManager;
+
+    @Autowired
+    private SecurityContextRepository securityContextRepository;
+	
 	@Bean
     public PasswordEncoder passwordEncoder() {
         return new BCryptPasswordEncoder();
@@ -27,7 +36,11 @@ public class WebConfigSecurity {
 		return http.csrf().disable()
 				   .formLogin().disable()
 				   .logout().disable()
+				   .authenticationManager(authenticationManager)
+	               .securityContextRepository(securityContextRepository)
 				   .authorizeExchange(exchanges -> exchanges
+					   .pathMatchers("/api/v1/authenticate").permitAll()
+					   .pathMatchers("/api/v1/refresh").permitAll()
 				       .anyExchange().authenticated()
 				   )
 				   .httpBasic()

+ 88 - 0
src/main/java/es/uv/garcosda/controllers/AuthorizationController.java

@@ -0,0 +1,88 @@
+package es.uv.garcosda.controllers;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ServerWebExchange;
+
+import es.uv.garcosda.dao.AuthenticationRequest;
+import es.uv.garcosda.services.JwtService;
+import es.uv.garcosda.services.UserService;
+import reactor.core.publisher.Mono;
+
+
+@RestController
+@RequestMapping("/api/v1")
+public class AuthorizationController {
+
+	@Autowired
+	private PasswordEncoder pe;
+	
+	@Autowired
+	private UserService us;
+	
+	@Autowired
+	private JwtService tp;
+	
+	@PostMapping("authenticate")
+    public Mono<ResponseEntity<?>> login(@RequestBody AuthenticationRequest req) {
+		
+		return us.findByUsername(req.getUsername())
+			     .map(user -> {
+							if (pe.matches(req.getPassword(), user.getPassword())) {
+								Map<String, String> tokens = new HashMap<>();
+								String accessToken = this.tp.generateAccessToken(user.getUsername(), Arrays.asList(user.getRole()));
+								String refreshToken = this.tp.generateRefreshToken(user.getUsername(), Arrays.asList(user.getRole()));
+								tokens.put("access_token", accessToken);
+								tokens.put("refresh_token", refreshToken);
+								HttpHeaders headers = new HttpHeaders();
+							    headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+							    headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
+								return ResponseEntity.ok()
+													 .headers(headers)
+													 .body(tokens);
+							} 
+							else {
+								return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
+							}
+				}).defaultIfEmpty(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build());
+	}
+	
+	@PostMapping("refresh")
+    public Mono<ResponseEntity<?>> refresh(ServerWebExchange exchange) {
+		String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
+		try {
+			String refreshToken = tp.getTokenFromHeader(header);
+			if(!tp.isTokenExpired(tp.getTokenFromHeader(header))) {
+				String accessToken = this.tp.generateAccessToken(tp.getUsernameFromToken(refreshToken), Arrays.asList(tp.getRolesFromToken(refreshToken)));
+				HttpHeaders headers = new HttpHeaders();
+			    headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+			    headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
+			    Map<String, String> tokens = new HashMap<>();
+			    tokens.put("access_token", accessToken);
+				tokens.put("refresh_token", refreshToken);
+				return Mono.just(ResponseEntity.ok()
+						                       .headers(headers)
+						                       .body(tokens));
+			}
+			else {
+				return Mono.just(ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token"));
+			}
+		}
+		catch(Exception e){
+			return Mono.just(ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token"));
+		}
+	}
+	
+}

+ 37 - 0
src/main/java/es/uv/garcosda/dao/AuthenticationRequest.java

@@ -0,0 +1,37 @@
+package es.uv.garcosda.dao;
+
+public class AuthenticationRequest {
+	private String username;
+    private String password;
+    
+    public AuthenticationRequest() {}
+    
+	public AuthenticationRequest(String username, String password) {
+		this.username = username;
+		this.password = password;
+	}
+
+	public String getUsername() {
+		return username;
+	}
+
+	public void setUsername(String username) {
+		this.username = username;
+	}
+
+	public String getPassword() {
+		return password;
+	}
+
+	public void setPassword(String password) {
+		this.password = password;
+	}
+    
+    
+    
+    
+    
+    
+}
+
+

+ 2 - 0
src/main/java/es/uv/garcosda/domain/Document.java

@@ -2,11 +2,13 @@ package es.uv.garcosda.domain;
 
 
 import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.mapping.Table;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
+@Table(name="DOCUMENT")
 public class Document {
 	
 	@Id

+ 43 - 0
src/main/java/es/uv/garcosda/security/AuthenticationManager.java

@@ -0,0 +1,43 @@
+package es.uv.garcosda.security;
+
+import java.util.Collection;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+
+import es.uv.garcosda.services.JwtService;
+import reactor.core.publisher.Mono;
+
+@Component
+public class AuthenticationManager implements ReactiveAuthenticationManager {
+
+	@Autowired
+	private JwtService tp;
+
+	@Override
+	public Mono<Authentication> authenticate(Authentication authentication) {
+		String token = authentication.getCredentials().toString();
+		String username;
+		try {
+			username = tp.getUsernameFromToken(token);
+		} 
+		catch (Exception e) {
+			username = null;
+		}
+		if (username != null && !tp.isTokenExpired(token)) {
+			Collection<SimpleGrantedAuthority> authorities = tp.getAuthoritiesFromToken(token);
+			UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, username, authorities);
+			SecurityContextHolder.getContext().setAuthentication(auth);
+			return Mono.just(auth);
+		} 
+		else {
+			return Mono.empty();
+		}
+	}
+}
+

+ 48 - 0
src/main/java/es/uv/garcosda/security/SecurityContextRepository.java

@@ -0,0 +1,48 @@
+package es.uv.garcosda.security;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.security.web.server.context.ServerSecurityContextRepository;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+
+import es.uv.garcosda.services.JwtService;
+import reactor.core.publisher.Mono;
+
+@Component
+public class SecurityContextRepository implements ServerSecurityContextRepository {
+
+	@Autowired
+	private AuthenticationManager am;
+	@Autowired
+	private JwtService tp;
+
+	@Override
+	public Mono<Void> save(ServerWebExchange swe, SecurityContext sc) {
+		throw new UnsupportedOperationException("Not supported yet.");
+	}
+
+	@Override
+	public Mono<SecurityContext> load(ServerWebExchange swe) {
+		ServerHttpRequest request = swe.getRequest();
+		String header = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
+		String authToken = null;
+		if (header != null && header.startsWith(this.tp.getTokenHeaderPrefix())) {
+			authToken = this.tp.getTokenFromHeader(header);
+		}	
+		if (authToken != null) {
+			Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
+			return this.am.authenticate(auth)
+					      .map((authentication) -> new SecurityContextImpl(authentication));
+		} 
+		else {
+			return Mono.empty();
+		}
+	}
+
+}

+ 90 - 0
src/main/java/es/uv/garcosda/services/JwtService.java

@@ -0,0 +1,90 @@
+package es.uv.garcosda.services;
+
+import jakarta.annotation.PostConstruct;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.stereotype.Component;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.JWTVerifier;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.interfaces.DecodedJWT;
+
+@Component
+public class JwtService {
+
+	@Value("${sys.token.key}")
+	private String key;
+	
+	@Value("${sys.token.issuer}")
+	private String issuer;
+	
+	@Value("${sys.token.duration}")
+	private Integer duration;
+	
+	private Algorithm algorithm;
+	private JWTVerifier verifier;
+	
+	@PostConstruct
+	public void init(){
+		this.algorithm = Algorithm.HMAC256(this.key.getBytes());
+		this.verifier = JWT.require(this.algorithm).build();
+	}
+	
+	public String generateAccessToken(String username, List<String> claims) {
+		return JWT.create()
+				 .withSubject(username)
+				 .withExpiresAt(new Date(System.currentTimeMillis()+this.duration))
+				 .withIssuer(this.issuer)
+				 .withClaim("roles", claims)
+				 .sign(this.algorithm);
+	}
+	
+	public String generateRefreshToken(String username, List<String> claims) {
+		return JWT.create()
+				 .withSubject(username)
+				 .withExpiresAt(new Date(System.currentTimeMillis()+(this.duration*2)))
+				 .withIssuer(this.issuer)
+				 .withClaim("roles", claims)
+				 .sign(this.algorithm);
+	}
+	
+	public String getUsernameFromToken(String token) {
+		DecodedJWT decoded = this.verifier.verify(token);
+		return decoded.getSubject();
+	}
+	
+	public Collection<SimpleGrantedAuthority> getAuthoritiesFromToken(String token){
+		DecodedJWT decoded = this.verifier.verify(token);
+		String[] roles = decoded.getClaim("roles").asArray(String.class);
+		Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
+		for(String r : roles) { authorities.add(new SimpleGrantedAuthority(r)); }
+		return authorities;
+	}
+	
+	public String[] getRolesFromToken(String token){
+		DecodedJWT decoded = this.verifier.verify(token);
+		String[] roles = decoded.getClaim("roles").asArray(String.class);
+		return roles;
+	}
+	
+	public Boolean isTokenExpired(String token) {
+		DecodedJWT decoded = this.verifier.verify(token);
+        final Date expiration = decoded.getExpiresAt();
+        return expiration.before(new Date());
+    }
+	
+	public String getTokenFromHeader(String header) {
+		return header.substring(this.getTokenHeaderPrefix().length());
+	}
+	
+	public String getTokenHeaderPrefix() {
+		return "Bearer ";
+	}
+}

+ 1 - 1
src/main/java/es/uv/garcosda/services/UserService.java

@@ -40,4 +40,4 @@ public class UserService {
 		return this.ur.deleteAll();
 	}
 	
-}
+}

+ 5 - 0
src/main/resources/application.properties

@@ -9,5 +9,10 @@ spring.h2.console.enabled=true
 
 #logging.level.io.r2dbc.h2=TRACE
 
+# token provider configuration
+sys.token.issuer=News service
+sys.token.key=MySuperSecureEncriptedAndProtectedKey
+sys.token.duration=600000
+