Socket authentication works

This commit is contained in:
Claudio Maggioni 2020-03-15 10:44:10 +01:00
parent 34dce54575
commit 3c034f56d1
10 changed files with 320 additions and 155 deletions

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

@ -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

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

View file

@ -1,64 +1,84 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; package ch.usi.inf.sa4.sanmarinoes.smarthut.socket;
import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtil; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.didThrow;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; 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 com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.util.*; import java.util.*;
import javax.websocket.*; import javax.websocket.*;
import org.springframework.beans.factory.annotation.Autowired; 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 { public class SensorSocketEndpoint extends Endpoint {
private Gson gson = new Gson(); private Gson gson = new Gson();
@Autowired private JWTTokenUtil jwtTokenUtil; private AuthenticationMessageListener authenticationMessageListener;
private Set<Session> unauthorizedClients = Collections.synchronizedSet(new HashSet<Session>()); private Set<Session> unauthorizedClients = Collections.synchronizedSet(new HashSet<>());
// commented out because of script not letting me push private Multimap<User, Session> authorizedClients =
// private Map< User, Set<Session> > authorizedClients = Collections.synchronizedMap( Multimaps.synchronizedMultimap(HashMultimap.create());
// new HashMap< User, HashSet<Session> >
// );
@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() { public Set<Session> getUnauthorizedClients() {
return unauthorizedClients; return unauthorizedClients;
} }
public Map<User, Set<Session>> getAuthorizedClients() { /**
* 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; return authorizedClients;
} }
public int broadcast(JsonObject message) throws IOException, EncodeException { /**
for (Session session : unauthorizedClients) { * Given a message and a user, broadcasts that message in json to all associated clients and
System.out.println(message); * returns the number of successful transfers
session.getBasicRemote().sendObject(message); *
} * @param message the message to send
return unauthorizedClients.size(); * @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 @Override
public void onOpen(Session session, EndpointConfig config) { public void onOpen(Session session, EndpointConfig config) {
final JsonObject test = new JsonObject();
test.addProperty("ciao", "mamma");
try {
session.getBasicRemote().sendText(gson.toJson(test));
unauthorizedClients.add(session); unauthorizedClients.add(session);
} catch (IOException e) { session.addMessageHandler(
e.printStackTrace(); authenticationMessageListener.newHandler(
} session,
} (u, s) -> {
unauthorizedClients.remove(s);
@OnMessage authorizedClients.put(u, s);
public void onMessage(String message) { }));
if (message != null) {
if (message.contains("Bearer: ")) {
String token = message.substring(message.lastIndexOf("Bearer "));
String username = jwtTokenUtil.getUsernameFromToken(token);
} else {
}
}
} }
} }

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;
}
};
}
} }

View file

@ -1,26 +0,0 @@
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script>
let connection = new WebSocket("ws://localhost:8080/sensor-socket", ["access_token", "malusa"]);
console.log("***CREATED WEBSOCKET");
connection.onopen = function(evt) {
console.log("***ONOPEN", evt);
connection.send({ciao: "mamma"});
};
connection.onmessage = function(evt) {
console.log("***ONMESSAGE", evt);
};
connection.onerror = function(evt) {
console.error("***ONERROR", evt);
};
console.log("***CREATED all");
</script>
</body>
</html>