From 589ef8c3cc4de247c65d59717d4b10004427a720 Mon Sep 17 00:00:00 2001 From: Claudio Maggioni Date: Mon, 9 Mar 2020 23:38:57 +0100 Subject: [PATCH 1/4] Imported code from secret source for websockets (@tommi27 you don't know anything about it, right?) --- .../smarthut/socket/SensorSocketConfig.java | 26 ++++++++++++ .../smarthut/socket/SensorSocketDecoder.java | 32 +++++++++++++++ .../smarthut/socket/SensorSocketEncoder.java | 24 +++++++++++ .../smarthut/socket/SensorSocketEndpoint.java | 41 +++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java create mode 100644 src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketDecoder.java create mode 100644 src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEncoder.java create mode 100644 src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java new file mode 100644 index 0000000..7d04538 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java @@ -0,0 +1,26 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; + +import javax.websocket.server.ServerEndpointConfig; + +public class SensorSocketConfig extends ServerEndpointConfig.Configurator { + + public static SensorSocketEndpoint getInstance() { + return instance; + } + + private static SensorSocketEndpoint instance = new SensorSocketEndpoint(); + + @Override + public T getEndpointInstance(Class endpointClass) throws InstantiationException { + try { + @SuppressWarnings("unchecked") + final T instance = (T) SensorSocketConfig.instance; + return instance; + } catch (ClassCastException e) { + final var e2 = + new InstantiationException("Cannot cast SensorSocketEndpoint to desired type"); + e2.initCause(e); + throw e2; + } + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketDecoder.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketDecoder.java new file mode 100644 index 0000000..ad3a76e --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketDecoder.java @@ -0,0 +1,32 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import javax.websocket.*; + +public class SensorSocketDecoder implements Decoder.Text { + private Gson decoder; + + @Override + public void init(EndpointConfig endpointConfig) { + decoder = new Gson(); + } + + @Override + public void destroy() {} + + @Override + public JsonObject decode(String s) throws DecodeException { + try { + return decoder.fromJson(s, JsonObject.class); + } catch (JsonSyntaxException e) { + throw new DecodeException(s, "Cannot decode sensor message", e); + } + } + + @Override + public boolean willDecode(String s) { + return true; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEncoder.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEncoder.java new file mode 100644 index 0000000..be04293 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEncoder.java @@ -0,0 +1,24 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import javax.websocket.EncodeException; +import javax.websocket.Encoder; +import javax.websocket.EndpointConfig; + +public class SensorSocketEncoder implements Encoder.Text { + private Gson encoder; + + @Override + public String encode(JsonObject object) throws EncodeException { + return encoder.toJson(object); + } + + @Override + public void init(EndpointConfig endpointConfig) { + encoder = new Gson(); + } + + @Override + public void destroy() {} +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java new file mode 100644 index 0000000..82ab767 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java @@ -0,0 +1,41 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; + +import com.google.gson.JsonObject; +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import javax.websocket.*; +import javax.websocket.server.ServerEndpoint; + +@ServerEndpoint( + value = "/service", + configurator = SensorSocketConfig.class, + encoders = SensorSocketEncoder.class, + decoders = SensorSocketDecoder.class) +public class SensorSocketEndpoint { + + private Set clients = Collections.synchronizedSet(new HashSet<>()); + + public Set getClients() { + return clients; + } + + @OnOpen + public void onOpen(Session userSession) { + clients.add(userSession); + } + + @OnClose + public void onClose(Session userSession) { + clients.remove(userSession); + } + + public int broadcast(JsonObject message) throws IOException, EncodeException { + for (Session session : clients) { + System.out.println(message); + session.getBasicRemote().sendObject(message); + } + return clients.size(); + } +} From 707291e637adcd70781dbd8496f41356780a1027 Mon Sep 17 00:00:00 2001 From: Claudio Maggioni Date: Wed, 11 Mar 2020 16:23:58 +0100 Subject: [PATCH 2/4] Unauthenticated socket works --- build.gradle | 2 + gradle.yml | 39 +++++++++++++++++++ .../smarthut/config/WebSecurityConfig.java | 1 + .../smarthut/socket/SensorSocketConfig.java | 15 +++++++ .../smarthut/socket/SensorSocketDecoder.java | 32 --------------- .../smarthut/socket/SensorSocketEncoder.java | 24 ------------ .../smarthut/socket/SensorSocketEndpoint.java | 37 ++++++++---------- test.html | 26 +++++++++++++ 8 files changed, 100 insertions(+), 76 deletions(-) create mode 100644 gradle.yml delete mode 100644 src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketDecoder.java delete mode 100644 src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEncoder.java create mode 100644 test.html diff --git a/build.gradle b/build.gradle index d23270b..5140ea7 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' 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 'org.springframework.security:spring-security-web' implementation 'org.postgresql:postgresql' diff --git a/gradle.yml b/gradle.yml new file mode 100644 index 0000000..51fefa4 --- /dev/null +++ b/gradle.yml @@ -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 + diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java index 253998d..ec116c3 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java @@ -51,6 +51,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // dont authenticate this particular request .authorizeRequests() .antMatchers( + "/sensor-socket", "/auth/login", "/swagger-ui.html", "/register", diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java index 7d04538..4e032bf 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java @@ -1,7 +1,12 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; import javax.websocket.server.ServerEndpointConfig; +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; +@Configuration public class SensorSocketConfig extends ServerEndpointConfig.Configurator { public static SensorSocketEndpoint getInstance() { @@ -10,6 +15,16 @@ public class SensorSocketConfig extends ServerEndpointConfig.Configurator { private static SensorSocketEndpoint instance = new SensorSocketEndpoint(); + @Bean + public ServerEndpointRegistration sensorSocketEndpoint() { + return new ServerEndpointRegistration("/sensor-socket", instance); + } + + @Bean + public ServerEndpointExporter endpointExporter() { + return new ServerEndpointExporter(); + } + @Override public T getEndpointInstance(Class endpointClass) throws InstantiationException { try { diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketDecoder.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketDecoder.java deleted file mode 100644 index ad3a76e..0000000 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketDecoder.java +++ /dev/null @@ -1,32 +0,0 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.google.gson.JsonSyntaxException; -import javax.websocket.*; - -public class SensorSocketDecoder implements Decoder.Text { - private Gson decoder; - - @Override - public void init(EndpointConfig endpointConfig) { - decoder = new Gson(); - } - - @Override - public void destroy() {} - - @Override - public JsonObject decode(String s) throws DecodeException { - try { - return decoder.fromJson(s, JsonObject.class); - } catch (JsonSyntaxException e) { - throw new DecodeException(s, "Cannot decode sensor message", e); - } - } - - @Override - public boolean willDecode(String s) { - return true; - } -} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEncoder.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEncoder.java deleted file mode 100644 index be04293..0000000 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEncoder.java +++ /dev/null @@ -1,24 +0,0 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import javax.websocket.EncodeException; -import javax.websocket.Encoder; -import javax.websocket.EndpointConfig; - -public class SensorSocketEncoder implements Encoder.Text { - private Gson encoder; - - @Override - public String encode(JsonObject object) throws EncodeException { - return encoder.toJson(object); - } - - @Override - public void init(EndpointConfig endpointConfig) { - encoder = new Gson(); - } - - @Override - public void destroy() {} -} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java index 82ab767..36565c8 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java @@ -1,19 +1,14 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; +import com.google.gson.Gson; import com.google.gson.JsonObject; import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; +import java.util.*; import javax.websocket.*; -import javax.websocket.server.ServerEndpoint; -@ServerEndpoint( - value = "/service", - configurator = SensorSocketConfig.class, - encoders = SensorSocketEncoder.class, - decoders = SensorSocketDecoder.class) -public class SensorSocketEndpoint { +public class SensorSocketEndpoint extends Endpoint { + + private Gson gson = new Gson(); private Set clients = Collections.synchronizedSet(new HashSet<>()); @@ -21,16 +16,6 @@ public class SensorSocketEndpoint { return clients; } - @OnOpen - public void onOpen(Session userSession) { - clients.add(userSession); - } - - @OnClose - public void onClose(Session userSession) { - clients.remove(userSession); - } - public int broadcast(JsonObject message) throws IOException, EncodeException { for (Session session : clients) { System.out.println(message); @@ -38,4 +23,16 @@ public class SensorSocketEndpoint { } return clients.size(); } + + @Override + public void onOpen(Session session, EndpointConfig config) { + final JsonObject test = new JsonObject(); + test.addProperty("ciao", "mamma"); + try { + session.getBasicRemote().sendText(gson.toJson(test)); + clients.add(session); + } catch (IOException e) { + e.printStackTrace(); + } + } } diff --git a/test.html b/test.html new file mode 100644 index 0000000..42bb82d --- /dev/null +++ b/test.html @@ -0,0 +1,26 @@ + + + + + + + + From 34dce5457576e3ee9acb1e629c103f96a6d439ba Mon Sep 17 00:00:00 2001 From: tommi27 Date: Sat, 14 Mar 2020 20:17:47 +0100 Subject: [PATCH 3/4] wip --- .../smarthut/socket/SensorSocketEndpoint.java | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java index 36565c8..2fde5df 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java @@ -1,27 +1,41 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtil; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; import com.google.gson.Gson; import com.google.gson.JsonObject; import java.io.IOException; import java.util.*; import javax.websocket.*; +import org.springframework.beans.factory.annotation.Autowired; public class SensorSocketEndpoint extends Endpoint { private Gson gson = new Gson(); - private Set clients = Collections.synchronizedSet(new HashSet<>()); + @Autowired private JWTTokenUtil jwtTokenUtil; - public Set getClients() { - return clients; + private Set unauthorizedClients = Collections.synchronizedSet(new HashSet()); + + // commented out because of script not letting me push + // private Map< User, Set > authorizedClients = Collections.synchronizedMap( + // new HashMap< User, HashSet > + // ); + + public Set getUnauthorizedClients() { + return unauthorizedClients; + } + + public Map> getAuthorizedClients() { + return authorizedClients; } public int broadcast(JsonObject message) throws IOException, EncodeException { - for (Session session : clients) { + for (Session session : unauthorizedClients) { System.out.println(message); session.getBasicRemote().sendObject(message); } - return clients.size(); + return unauthorizedClients.size(); } @Override @@ -30,9 +44,21 @@ public class SensorSocketEndpoint extends Endpoint { test.addProperty("ciao", "mamma"); try { session.getBasicRemote().sendText(gson.toJson(test)); - clients.add(session); + unauthorizedClients.add(session); } catch (IOException e) { e.printStackTrace(); } } + + @OnMessage + 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 { + + } + } + } } From 3c034f56d11fd197208358a3bfb0d3897634884c Mon Sep 17 00:00:00 2001 From: Claudio Maggioni Date: Sun, 15 Mar 2020 10:44:10 +0100 Subject: [PATCH 4/4] Socket authentication works --- socket_test.html | 36 +++++++ .../smarthut/config/JWTRequestFilter.java | 17 ++-- .../smarthut/config/JWTTokenUtil.java | 72 -------------- .../smarthut/config/JWTTokenUtils.java | 84 ++++++++++++++++ .../controller/AuthenticationController.java | 10 +- .../socket/AuthenticationMessageListener.java | 96 +++++++++++++++++++ .../smarthut/socket/SensorSocketConfig.java | 25 +++-- .../smarthut/socket/SensorSocketEndpoint.java | 92 +++++++++++------- .../sa4/sanmarinoes/smarthut/utils/Utils.java | 17 +++- test.html | 26 ----- 10 files changed, 320 insertions(+), 155 deletions(-) create mode 100644 socket_test.html delete mode 100644 src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtil.java create mode 100644 src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java create mode 100644 src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/AuthenticationMessageListener.java delete mode 100644 test.html diff --git a/socket_test.html b/socket_test.html new file mode 100644 index 0000000..90feba2 --- /dev/null +++ b/socket_test.html @@ -0,0 +1,36 @@ + + + + + +
+

Waiting for authentication...

+
+ + + diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java index 853083b..e0cbb6a 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java @@ -20,7 +20,7 @@ public class JWTRequestFilter extends OncePerRequestFilter { @Autowired private JWTUserDetailsService jwtUserDetailsService; - @Autowired private JWTTokenUtil jwtTokenUtil; + @Autowired private JWTTokenUtils jwtTokenUtils; @Override protected void doFilterInternal( @@ -30,13 +30,11 @@ public class JWTRequestFilter extends OncePerRequestFilter { String username = null; String jwtToken = null; - // JWT Token is in th - // e form "Bearer token". Remove Bearer word and get - // only the Token + // 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); + username = jwtTokenUtils.getUsernameFromToken(jwtToken); } catch (IllegalArgumentException e) { System.out.println("Unable to get JWT Token"); } catch (ExpiredJwtException e) { @@ -44,14 +42,15 @@ public class JWTRequestFilter extends OncePerRequestFilter { } } else { 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) { UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername( username); // if token is valid configure Spring Security to manually - // set - // authentication - if (jwtTokenUtil.validateToken(jwtToken, userDetails)) { + // set authentication + if (jwtTokenUtils.validateToken(jwtToken, userDetails)) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtil.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtil.java deleted file mode 100644 index 40d369f..0000000 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtil.java +++ /dev/null @@ -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 getClaimFromToken(String token, Function 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 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 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)); - } -} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java new file mode 100644 index 0000000..f6943a8 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java @@ -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 getClaimFromToken(String token, Function claimsResolver) { + final Claims claims = getAllClaimsFromToken(token); + return claimsResolver.apply(claims); + } + + private Claims getAllClaimsFromToken(String token) { + return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java index 1a1e266..3160e1c 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java @@ -1,6 +1,6 @@ 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.JWTResponse; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserUpdateRequest; @@ -26,7 +26,7 @@ public class AuthenticationController { private final UserRepository userRepository; - private final JWTTokenUtil jwtTokenUtil; + private final JWTTokenUtils jwtTokenUtils; private final JWTUserDetailsService userDetailsService; @@ -35,11 +35,11 @@ public class AuthenticationController { public AuthenticationController( AuthenticationManager authenticationManager, UserRepository userRepository, - JWTTokenUtil jwtTokenUtil, + JWTTokenUtils jwtTokenUtils, JWTUserDetailsService userDetailsService) { this.authenticationManager = authenticationManager; this.userRepository = userRepository; - this.jwtTokenUtil = jwtTokenUtil; + this.jwtTokenUtils = jwtTokenUtils; this.userDetailsService = userDetailsService; } @@ -68,7 +68,7 @@ public class AuthenticationController { authenticationRequest.getUsernameOrEmail()); } - final String token = jwtTokenUtil.generateToken(userDetails); + final String token = jwtTokenUtils.generateToken(userDetails); return new JWTResponse(token); } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/AuthenticationMessageListener.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/AuthenticationMessageListener.java new file mode 100644 index 0000000..e0b9249 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/AuthenticationMessageListener.java @@ -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 newHandler( + final Session session, BiConsumer 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(); + } + } + }; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java index 4e032bf..503667a 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java @@ -1,25 +1,38 @@ 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 { - public static SensorSocketEndpoint getInstance() { - return instance; + private SensorSocketEndpoint 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 - public ServerEndpointRegistration sensorSocketEndpoint() { + public ServerEndpointRegistration serverEndpointRegistration() { return new ServerEndpointRegistration("/sensor-socket", instance); } + /** + * Returns a new ServerEndpointExporter + * + * @return a new ServerEndpointExporter + */ @Bean public ServerEndpointExporter endpointExporter() { return new ServerEndpointExporter(); @@ -29,7 +42,7 @@ public class SensorSocketConfig extends ServerEndpointConfig.Configurator { public T getEndpointInstance(Class endpointClass) throws InstantiationException { try { @SuppressWarnings("unchecked") - final T instance = (T) SensorSocketConfig.instance; + final T instance = (T) this.instance; return instance; } catch (ClassCastException e) { final var e2 = diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java index 2fde5df..d40e874 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java @@ -1,64 +1,84 @@ 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 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.JsonObject; -import java.io.IOException; 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(); - @Autowired private JWTTokenUtil jwtTokenUtil; + private AuthenticationMessageListener authenticationMessageListener; - private Set unauthorizedClients = Collections.synchronizedSet(new HashSet()); + private Set unauthorizedClients = Collections.synchronizedSet(new HashSet<>()); - // commented out because of script not letting me push - // private Map< User, Set > authorizedClients = Collections.synchronizedMap( - // new HashMap< User, HashSet > - // ); + private Multimap 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 getUnauthorizedClients() { return unauthorizedClients; } - public Map> getAuthorizedClients() { + /** + * Returns a synchronized User to Session multimap with authorized sessions + * + * @return a synchronized User to Session multimap with authorized sessions + */ + public Multimap getAuthorizedClients() { return authorizedClients; } - public int broadcast(JsonObject message) throws IOException, EncodeException { - for (Session session : unauthorizedClients) { - System.out.println(message); - session.getBasicRemote().sendObject(message); - } - return unauthorizedClients.size(); + /** + * 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 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) { - final JsonObject test = new JsonObject(); - test.addProperty("ciao", "mamma"); - try { - session.getBasicRemote().sendText(gson.toJson(test)); - unauthorizedClients.add(session); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @OnMessage - 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 { - - } - } + unauthorizedClients.add(session); + session.addMessageHandler( + authenticationMessageListener.newHandler( + session, + (u, s) -> { + unauthorizedClients.remove(s); + authorizedClients.put(u, s); + })); } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java index d9fcf12..bc8719d 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java @@ -1,14 +1,29 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.utils; 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.StreamSupport; /** A class with a bunch of useful static methods */ -public class Utils { +public final class Utils { private Utils() {} public static List toList(Iterable iterable) { return StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList()); } + + public static Predicate didThrow(Function> consumer) { + return (t) -> { + try { + consumer.apply(t).get(); + return true; + } catch (Throwable e) { + System.err.println(e.getMessage()); + return false; + } + }; + } } diff --git a/test.html b/test.html deleted file mode 100644 index 42bb82d..0000000 --- a/test.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - -