Merge branch 'websockets-feature' into 'dev'

Import and adapt code from secret source for websockets (@tommi27 you don't know anything about it, right?)

See merge request sa4-2020/the-sanmarinoes/backend!30
This commit is contained in:
Claudio Maggioni 2020-03-15 11:08:41 +01:00
commit fd6103b6da
12 changed files with 425 additions and 87 deletions

View file

@ -23,6 +23,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework:spring-websocket:5.2.4.RELEASE'
implementation 'io.jsonwebtoken:jjwt:0.9.1' implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'org.springframework.security:spring-security-web' implementation 'org.springframework.security:spring-security-web'
implementation 'org.postgresql:postgresql' implementation 'org.postgresql:postgresql'

39
gradle.yml Normal file
View file

@ -0,0 +1,39 @@
# vim: set ts=2 sw=2 et tw=80:
image: gradle:jdk13
stages:
- build
- test
- deploy
smarthut_build:
stage: build
script:
- gradle assemble
artifacts:
paths:
- build/libs/*.jar
expire_in: 1 week
smarthut_test:
stage: test
script:
- gradle check
smarthut_deploy:
stage: deploy
image: docker:latest
services:
- docker:dind
variables:
DOCKER_DRIVER: overlay
before_script:
- docker version
- docker info
- docker login -u smarthutsm -p $CI_DOCKER_PASS
script:
- "docker build -t smarthutsm/smarthut:${CI_COMMIT_BRANCH} --pull ."
- "docker push smarthutsm/smarthut:${CI_COMMIT_BRANCH}"
after_script:
- docker logout

36
socket_test.html Normal file
View file

@ -0,0 +1,36 @@
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="malusa">
<h1>Waiting for authentication...</h1>
</div>
<script>
let malusa = document.getElementById("malusa");
let connection = new WebSocket("ws://localhost:8080/sensor-socket");
console.log("***CREATED WEBSOCKET");
connection.onopen = function(evt) {
console.log("***ONOPEN", evt);
connection.send(JSON.stringify({token: prompt("insert authentication token")}));
};
connection.onmessage = function(evt) {
console.log("***ONMESSAGE", evt);
let data = JSON.parse(evt.data);
if (data.authenticated) {
malusa.innerHTML = "<h1>Socket is now authenticated!</h1>" +
"<img src='https://maggioni.xyz/astley.gif'>";
} else if (data.authenticated === false) {
malusa.innerHTML = "<h1>Authentication error</h1>";
}
};
connection.onerror = function(evt) {
console.error("***ONERROR", evt);
};
</script>
</body>
</html>

View file

@ -20,7 +20,7 @@ public class JWTRequestFilter extends OncePerRequestFilter {
@Autowired private JWTUserDetailsService jwtUserDetailsService; @Autowired private JWTUserDetailsService jwtUserDetailsService;
@Autowired private JWTTokenUtil jwtTokenUtil; @Autowired private JWTTokenUtils jwtTokenUtils;
@Override @Override
protected void doFilterInternal( protected void doFilterInternal(
@ -30,13 +30,11 @@ public class JWTRequestFilter extends OncePerRequestFilter {
String username = null; String username = null;
String jwtToken = null; String jwtToken = null;
// JWT Token is in th // JWT Token is in the form "Bearer token". Remove Bearer word and get only the Token
// e form "Bearer token". Remove Bearer word and get
// only the Token
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7); jwtToken = requestTokenHeader.substring(7);
try { try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken); username = jwtTokenUtils.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
System.out.println("Unable to get JWT Token"); System.out.println("Unable to get JWT Token");
} catch (ExpiredJwtException e) { } catch (ExpiredJwtException e) {
@ -44,14 +42,15 @@ public class JWTRequestFilter extends OncePerRequestFilter {
} }
} else { } else {
logger.warn("JWT Token does not begin with Bearer String"); logger.warn("JWT Token does not begin with Bearer String");
} // Once we get the token validate it. }
// Once we get the token validate it.
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = UserDetails userDetails =
this.jwtUserDetailsService.loadUserByUsername( this.jwtUserDetailsService.loadUserByUsername(
username); // if token is valid configure Spring Security to manually username); // if token is valid configure Spring Security to manually
// set // set authentication
// authentication if (jwtTokenUtils.validateToken(jwtToken, userDetails)) {
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken( new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()); userDetails, null, userDetails.getAuthorities());

View file

@ -1,72 +0,0 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
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;
@Component
public class JWTTokenUtil {
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));
}
}

View file

@ -0,0 +1,84 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
/** A utility class to handle JWTs */
@Component
public class JWTTokenUtils {
/** The duration in seconds of the validity of a single token */
private static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
/** The secret key used to encrypt all JWTs */
@Value("${jwt.secret}")
private String secret;
/**
* Retrieves the claimed username from a given token
*
* @param token the token to inspect
* @return the username
*/
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
/**
* Returns whether the token given is expired or not
*
* @param token the given token
* @return true if expired, false if not
*/
public Boolean isTokenExpired(String token) {
final Date expiration = getClaimFromToken(token, Claims::getExpiration);
return expiration.before(new Date());
}
/**
* Creates a new JWT for a given user. 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
*
* @param user the user to which create a JWT
* @return the newly generated token
*/
public String generateToken(UserDetails user) {
return Jwts.builder()
.setClaims(new HashMap<>())
.setSubject(user.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* Validates the token given against matching userDetails
*
* @param token the token given
* @param userDetails user details to validate against
* @return true if valid, false if not
*/
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
}

View file

@ -51,6 +51,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// dont authenticate this particular request // dont authenticate this particular request
.authorizeRequests() .authorizeRequests()
.antMatchers( .antMatchers(
"/sensor-socket",
"/auth/login", "/auth/login",
"/swagger-ui.html", "/swagger-ui.html",
"/register", "/register",

View file

@ -1,6 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtil; import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTResponse; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTResponse;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserUpdateRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserUpdateRequest;
@ -26,7 +26,7 @@ public class AuthenticationController {
private final UserRepository userRepository; private final UserRepository userRepository;
private final JWTTokenUtil jwtTokenUtil; private final JWTTokenUtils jwtTokenUtils;
private final JWTUserDetailsService userDetailsService; private final JWTUserDetailsService userDetailsService;
@ -35,11 +35,11 @@ public class AuthenticationController {
public AuthenticationController( public AuthenticationController(
AuthenticationManager authenticationManager, AuthenticationManager authenticationManager,
UserRepository userRepository, UserRepository userRepository,
JWTTokenUtil jwtTokenUtil, JWTTokenUtils jwtTokenUtils,
JWTUserDetailsService userDetailsService) { JWTUserDetailsService userDetailsService) {
this.authenticationManager = authenticationManager; this.authenticationManager = authenticationManager;
this.userRepository = userRepository; this.userRepository = userRepository;
this.jwtTokenUtil = jwtTokenUtil; this.jwtTokenUtils = jwtTokenUtils;
this.userDetailsService = userDetailsService; this.userDetailsService = userDetailsService;
} }
@ -68,7 +68,7 @@ public class AuthenticationController {
authenticationRequest.getUsernameOrEmail()); authenticationRequest.getUsernameOrEmail());
} }
final String token = jwtTokenUtil.generateToken(userDetails); final String token = jwtTokenUtils.generateToken(userDetails);
return new JWTResponse(token); return new JWTResponse(token);
} }

View file

@ -0,0 +1,96 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.socket;
import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.jsonwebtoken.ExpiredJwtException;
import java.io.IOException;
import java.util.Map;
import java.util.function.BiConsumer;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/** Generates MessageHandlers for unauthenticated socket sessions */
@Component
public class AuthenticationMessageListener {
private Gson gson = new Gson();
private JWTTokenUtils jwtTokenUtils;
private UserRepository userRepository;
@Autowired
public AuthenticationMessageListener(
JWTTokenUtils jwtTokenUtils, UserRepository userRepository) {
this.jwtTokenUtils = jwtTokenUtils;
this.userRepository = userRepository;
}
/**
* Generates a new message handler to handle socket authentication
*
* @param session the session to which authentication must be checked
* @param authorizedSetter function to call once user is authenticated
* @return a new message handler to handle socket authentication
*/
MessageHandler.Whole<String> newHandler(
final Session session, BiConsumer<User, Session> authorizedSetter) {
return new MessageHandler.Whole<>() {
@Override
public void onMessage(final String message) {
System.out.println(message);
if (message == null) {
acknowledge(false);
return;
}
String token;
String username;
try {
token = gson.fromJson(message, JsonObject.class).get("token").getAsString();
username = jwtTokenUtils.getUsernameFromToken(token);
} catch (ExpiredJwtException e) {
System.err.println(e.getMessage());
acknowledge(false);
return;
} catch (Throwable ignored) {
System.out.println("Token format not valid");
acknowledge(false);
return;
}
final User user = userRepository.findByUsername(username);
if (user == null || jwtTokenUtils.isTokenExpired(token)) {
System.out.println("Token not valid");
acknowledge(false);
return;
}
// Here user is authenticated
session.removeMessageHandler(this);
// Add user-session pair in authorized list
authorizedSetter.accept(user, session);
// update client to acknowledge authentication
acknowledge(true);
}
private void acknowledge(boolean success) {
try {
session.getBasicRemote()
.sendText(gson.toJson(Map.of("authenticated", success)));
} catch (IOException e) {
e.printStackTrace();
}
}
};
}
}

View file

@ -0,0 +1,54 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.socket;
import javax.websocket.server.ServerEndpointConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import org.springframework.web.socket.server.standard.ServerEndpointRegistration;
/** Configures the sensor socket and maps it to the /sensor-socket path */
@Configuration
public class SensorSocketConfig extends ServerEndpointConfig.Configurator {
private SensorSocketEndpoint instance;
@Autowired
public SensorSocketConfig(SensorSocketEndpoint instance) {
this.instance = instance;
}
/**
* Registers the sensor socket endpoint to the url /sensor-socket
*
* @return an endpoint registration object
*/
@Bean
public ServerEndpointRegistration serverEndpointRegistration() {
return new ServerEndpointRegistration("/sensor-socket", instance);
}
/**
* Returns a new ServerEndpointExporter
*
* @return a new ServerEndpointExporter
*/
@Bean
public ServerEndpointExporter endpointExporter() {
return new ServerEndpointExporter();
}
@Override
public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {
try {
@SuppressWarnings("unchecked")
final T instance = (T) this.instance;
return instance;
} catch (ClassCastException e) {
final var e2 =
new InstantiationException("Cannot cast SensorSocketEndpoint to desired type");
e2.initCause(e);
throw e2;
}
}
}

View file

@ -0,0 +1,84 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.socket;
import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.didThrow;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.gson.Gson;
import java.util.*;
import javax.websocket.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/** Endpoint of socket at URL /sensor-socket used to update the client with sensor information */
@Component
public class SensorSocketEndpoint extends Endpoint {
private Gson gson = new Gson();
private AuthenticationMessageListener authenticationMessageListener;
private Set<Session> unauthorizedClients = Collections.synchronizedSet(new HashSet<>());
private Multimap<User, Session> authorizedClients =
Multimaps.synchronizedMultimap(HashMultimap.create());
@Autowired
public SensorSocketEndpoint(AuthenticationMessageListener authenticationMessageListener) {
this.authenticationMessageListener = authenticationMessageListener;
}
/**
* Returns a synchronized set of socket sessions not yet authorized with a token
*
* @return a synchronized set of socket sessions not yet authorized with a token
*/
public Set<Session> getUnauthorizedClients() {
return unauthorizedClients;
}
/**
* Returns a synchronized User to Session multimap with authorized sessions
*
* @return a synchronized User to Session multimap with authorized sessions
*/
public Multimap<User, Session> getAuthorizedClients() {
return authorizedClients;
}
/**
* Given a message and a user, broadcasts that message in json to all associated clients and
* returns the number of successful transfers
*
* @param message the message to send
* @param u the user to which to send the message
* @return number of successful transfer
*/
public long broadcast(Object message, User u) {
final Collection<Session> sessions = authorizedClients.get(u);
return sessions.stream()
.parallel()
.filter(didThrow(s -> s.getAsyncRemote().sendObject(gson.toJson(message))))
.count();
}
/**
* Handles the opening of a socket session with a client
*
* @param session the newly born session
* @param config endpoint configuration
*/
@Override
public void onOpen(Session session, EndpointConfig config) {
unauthorizedClients.add(session);
session.addMessageHandler(
authenticationMessageListener.newHandler(
session,
(u, s) -> {
unauthorizedClients.remove(s);
authorizedClients.put(u, s);
}));
}
}

View file

@ -1,14 +1,29 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.utils; package ch.usi.inf.sa4.sanmarinoes.smarthut.utils;
import java.util.List; import java.util.List;
import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
/** A class with a bunch of useful static methods */ /** A class with a bunch of useful static methods */
public class Utils { public final class Utils {
private Utils() {} private Utils() {}
public static <T> List<T> toList(Iterable<T> iterable) { public static <T> List<T> toList(Iterable<T> iterable) {
return StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList()); return StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList());
} }
public static <T> Predicate<T> didThrow(Function<T, Future<?>> consumer) {
return (t) -> {
try {
consumer.apply(t).get();
return true;
} catch (Throwable e) {
System.err.println(e.getMessage());
return false;
}
};
}
} }