From dbf34d353f78c3c032202014b77d13a5a211725a Mon Sep 17 00:00:00 2001 From: Daniel Gerhardt <code@dgerhardt.net> Date: Wed, 28 Feb 2018 13:49:58 +0100 Subject: [PATCH] Add support for JWT authentication --- pom.xml | 5 ++ .../jwt/JwtAuthenticationProvider.java | 27 +++++++ .../thm/arsnova/security/jwt/JwtService.java | 76 +++++++++++++++++++ .../de/thm/arsnova/security/jwt/JwtToken.java | 34 +++++++++ .../arsnova/security/jwt/JwtTokenFilter.java | 29 +++++++ src/main/resources/arsnova.properties.example | 6 ++ src/test/resources/arsnova.properties.example | 6 ++ 7 files changed, 183 insertions(+) create mode 100644 src/main/java/de/thm/arsnova/security/jwt/JwtAuthenticationProvider.java create mode 100644 src/main/java/de/thm/arsnova/security/jwt/JwtService.java create mode 100644 src/main/java/de/thm/arsnova/security/jwt/JwtToken.java create mode 100644 src/main/java/de/thm/arsnova/security/jwt/JwtTokenFilter.java diff --git a/pom.xml b/pom.xml index bdb5d2590..50a2777c1 100644 --- a/pom.xml +++ b/pom.xml @@ -250,6 +250,11 @@ <artifactId>junit</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + <version>3.4.0</version> + </dependency> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-oauth</artifactId> diff --git a/src/main/java/de/thm/arsnova/security/jwt/JwtAuthenticationProvider.java b/src/main/java/de/thm/arsnova/security/jwt/JwtAuthenticationProvider.java new file mode 100644 index 000000000..ac7d51b46 --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/jwt/JwtAuthenticationProvider.java @@ -0,0 +1,27 @@ +package de.thm.arsnova.security.jwt; + +import de.thm.arsnova.security.User; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +public class JwtAuthenticationProvider implements AuthenticationProvider { + private JwtService jwtService; + + public JwtAuthenticationProvider(final JwtService jwtService) { + this.jwtService = jwtService; + } + + @Override + public Authentication authenticate(final Authentication authentication) throws AuthenticationException { + final String token = (String) authentication.getCredentials(); + final User user = jwtService.verifyToken((String) authentication.getCredentials()); + + return new JwtToken(token, user, user.getAuthorities()); + } + + @Override + public boolean supports(final Class<?> aClass) { + return JwtToken.class.isAssignableFrom(aClass); + } +} diff --git a/src/main/java/de/thm/arsnova/security/jwt/JwtService.java b/src/main/java/de/thm/arsnova/security/jwt/JwtService.java new file mode 100644 index 000000000..f1620aaee --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/jwt/JwtService.java @@ -0,0 +1,76 @@ +package de.thm.arsnova.security.jwt; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import de.thm.arsnova.security.User; +import de.thm.arsnova.services.UserService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; + +import java.io.UnsupportedEncodingException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.temporal.TemporalAmount; +import java.util.Collection; +import java.util.Date; +import java.util.stream.Collectors; + +@Service +public class JwtService { + private static final String CONFIG_PREFIX = "security.jwt."; + private static final String ROLE_PREFIX = "ROLE_"; + private static final String ROLES_CLAIM_NAME = "roles"; + private Algorithm algorithm; + private String serverId; + private TemporalAmount validityPeriod; + private JWTVerifier verifier; + private UserService userService; + + public JwtService( + final UserService userService, + @Value("${" + CONFIG_PREFIX + "secret}") final String secret, + @Value("${" + CONFIG_PREFIX + "serverId}") final String serverId, + @Value("${" + CONFIG_PREFIX + "validity-period}") final String validityPeriod) + throws UnsupportedEncodingException { + this.userService = userService; + this.serverId = serverId; + try { + this.validityPeriod = Duration.parse("P" + validityPeriod); + } catch (Exception e) { + throw new IllegalArgumentException(validityPeriod, e); + } + algorithm = Algorithm.HMAC256(secret); + verifier = JWT.require(algorithm) + .withAudience(serverId) + .build(); + } + + public String createSignedToken(final User user) { + String[] roles = user.getAuthorities().stream() + .map(ga -> ga.getAuthority()) + .filter(ga -> ga.startsWith(ROLE_PREFIX)) + .map(ga -> ga.substring(ROLE_PREFIX.length())).toArray(String[]::new); + return JWT.create() + .withIssuer(serverId) + .withAudience(serverId) + .withIssuedAt(new Date()) + .withExpiresAt(Date.from(LocalDateTime.now().plus(validityPeriod).toInstant(ZoneOffset.UTC))) + .withSubject(user.getId()) + .withArrayClaim(ROLES_CLAIM_NAME, roles) + .sign(algorithm); + } + + public User verifyToken(final String token) { + final DecodedJWT decodedJwt = verifier.verify(token); + final String userId = decodedJwt.getSubject(); + final Collection<GrantedAuthority> authorities = decodedJwt.getClaim(ROLES_CLAIM_NAME).asList(String.class).stream() + .map(role -> new SimpleGrantedAuthority(ROLE_PREFIX + role)).collect(Collectors.toList()); + + return new User(userService.get(userId), authorities); + } +} diff --git a/src/main/java/de/thm/arsnova/security/jwt/JwtToken.java b/src/main/java/de/thm/arsnova/security/jwt/JwtToken.java new file mode 100644 index 000000000..f7ce5cea3 --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/jwt/JwtToken.java @@ -0,0 +1,34 @@ +package de.thm.arsnova.security.jwt; + +import de.thm.arsnova.security.User; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; +import java.util.Collections; + +public class JwtToken extends AbstractAuthenticationToken { + private String token; + private User principal; + + public JwtToken(final String token, final User principal, + final Collection<? extends GrantedAuthority> grantedAuthorities) { + super(grantedAuthorities); + this.token = token; + this.principal = principal; + } + + public JwtToken(final String token) { + this(token, null, Collections.emptyList()); + } + + @Override + public Object getCredentials() { + return token; + } + + @Override + public Object getPrincipal() { + return principal; + } +} diff --git a/src/main/java/de/thm/arsnova/security/jwt/JwtTokenFilter.java b/src/main/java/de/thm/arsnova/security/jwt/JwtTokenFilter.java new file mode 100644 index 000000000..282a3c16b --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/jwt/JwtTokenFilter.java @@ -0,0 +1,29 @@ +package de.thm.arsnova.security.jwt; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class JwtTokenFilter extends AbstractAuthenticationProcessingFilter { + private static final String JWT_HEADER_NAME = "Arsnova-Auth-Token"; + + protected JwtTokenFilter() { + super(new AntPathRequestMatcher("/**")); + } + + @Override + public Authentication attemptAuthentication(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse) throws AuthenticationException { + String jwtHeader = httpServletRequest.getHeader(JWT_HEADER_NAME); + if (jwtHeader == null) { + throw new PreAuthenticatedCredentialsNotFoundException("No authentication header present."); + } + JwtToken token = new JwtToken(jwtHeader); + + return getAuthenticationManager().authenticate(token); + } +} diff --git a/src/main/resources/arsnova.properties.example b/src/main/resources/arsnova.properties.example index b1135e4c3..0eda0cadf 100644 --- a/src/main/resources/arsnova.properties.example +++ b/src/main/resources/arsnova.properties.example @@ -59,6 +59,12 @@ mail.host= # several minutes security.authentication.login-try-limit=50 +# JSON Web Tokens +# +security.jwt.serverId=arsnova.backend.v3:com.example +security.jwt.secret= +security.jwt.validity-period=T6H + # Configuration parameters for authentication services: # enabled: enable or disable the service # allowed-roles: enable/disable service for a specific role (valid roles: speaker, student) diff --git a/src/test/resources/arsnova.properties.example b/src/test/resources/arsnova.properties.example index b1135e4c3..0eda0cadf 100644 --- a/src/test/resources/arsnova.properties.example +++ b/src/test/resources/arsnova.properties.example @@ -59,6 +59,12 @@ mail.host= # several minutes security.authentication.login-try-limit=50 +# JSON Web Tokens +# +security.jwt.serverId=arsnova.backend.v3:com.example +security.jwt.secret= +security.jwt.validity-period=T6H + # Configuration parameters for authentication services: # enabled: enable or disable the service # allowed-roles: enable/disable service for a specific role (valid roles: speaker, student) -- GitLab