package com.thinkbiganalytics.auth.jwt; /*- * #%L * thinkbig-security-auth * %% * Copyright (C) 2017 ThinkBig Analytics * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import com.thinkbiganalytics.auth.config.JwtProperties; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTimeUtils; import org.jose4j.jws.JsonWebSignature; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.MalformedClaimException; import org.jose4j.jwt.NumericDate; import org.jose4j.jwt.consumer.InvalidJwtException; import org.jose4j.jwt.consumer.JwtConsumer; import org.jose4j.jwt.consumer.JwtConsumerBuilder; import org.jose4j.keys.HmacKey; import org.jose4j.lang.JoseException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; import org.springframework.security.web.authentication.rememberme.InvalidCookieException; import java.security.Key; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Identifies previously remembered users by a JSON Web Token. * * <p>The token contains the user's names and groups. It is stored as a cookie in the user's browser to authenticate the user in subsequent requests.</p> */ public class JwtRememberMeServices extends AbstractRememberMeServices { /** * Key of the string list containing group names */ private static final String GROUPS = "groups"; /** * Identifies the signature algorithm */ @Nonnull private final String algorithmIdentifier; /** * Secret key for signature */ @Nullable private Key secretKey; /** * Constructs a {@code JwtRememberMeServices} with the specified configuration. * * @param properties the JWT configuration */ public JwtRememberMeServices(@Nonnull final JwtProperties properties) { super(properties.getKey(), username -> null); this.algorithmIdentifier = properties.getAlgorithm(); } /** * Decodes the specified JWT cookie into tokens. * * <p>The first element of the return value with be the JWT subject. The remaining elements are the elements in the {@code groups} list.</p> * * @param cookie the JWT cookie * @return an array with the username and group names * @throws IllegalStateException if the secret key is invalid * @throws InvalidCookieException if the cookie cannot be decoded */ @Nonnull @Override protected String[] decodeCookie(@Nonnull final String cookie) throws InvalidCookieException { // Build the JWT parser final JwtConsumer consumer = new JwtConsumerBuilder() .setEvaluationTime(NumericDate.fromMilliseconds(DateTimeUtils.currentTimeMillis())) .setVerificationKey(getSecretKey()) .build(); // Parse the cookie final String user; final List<String> groups; try { final JwtClaims claims = consumer.processToClaims(cookie); user = claims.getSubject(); groups = claims.getStringListClaimValue(GROUPS); } catch (final InvalidJwtException e) { throw new InvalidCookieException("JWT cookie is invalid: " + e); } catch (final MalformedClaimException e) { throw new InvalidCookieException("JWT cookie is malformed: " + cookie); } if (StringUtils.isBlank(user)) { throw new InvalidCookieException("Missing user in JWT cookie: " + cookie); } // Build the token array final Stream<String> userStream = Stream.of(user); final Stream<String> groupStream = groups.stream(); return Stream.concat(userStream, groupStream).toArray(String[]::new); } /** * Encodes the specified tokens into a JWT cookie. * * <p>The first element of {@code tokens} should be the user's principal. The remaining elements are the groups assigned to the user.</p> * * @param tokens an array with the username and group names * @return a JWT cookie * @throws IllegalStateException if the secret key is invalid */ @Nonnull @Override protected String encodeCookie(@Nonnull final String[] tokens) { // Determine expiration time final NumericDate expireTime = NumericDate.fromMilliseconds(DateTimeUtils.currentTimeMillis()); expireTime.addSeconds(getExpirationTimeSeconds()); // Build the JSON Web Token final JwtClaims claims = new JwtClaims(); claims.setExpirationTime(expireTime); claims.setSubject(tokens[0]); claims.setStringListClaim("groups", Arrays.asList(tokens).subList(1, tokens.length)); // Generate a signature final JsonWebSignature jws = new JsonWebSignature(); jws.setAlgorithmHeaderValue(algorithmIdentifier); jws.setKey(getSecretKey()); jws.setKeyIdHeaderValue(getSecretKey().getAlgorithm()); jws.setPayload(claims.toJson()); // Serialize the cookie try { return jws.getCompactSerialization(); } catch (final JoseException e) { throw new IllegalStateException("Unable to encode cookie: " + e, e); } } /** * Sets a JWT cookie when the user has successfully logged in. * * @param request the HTTP request * @param response the HTTP response * @param authentication the user */ @Override protected void onLoginSuccess(@Nonnull final HttpServletRequest request, @Nonnull final HttpServletResponse response, @Nonnull final Authentication authentication) { final Stream<String> user = Stream.of(authentication.getPrincipal().toString()); final Stream<String> groups = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority); final String[] tokens = Stream.concat(user, groups).toArray(String[]::new); setCookie(tokens, getTokenValiditySeconds(), request, response); } /** * Reconstructs the user from the specified tokens. * * @param tokens the tokens from the JWT cookie * @param request the HTTP request * @param response the HTTP response * @return the user */ @Override protected UserDetails processAutoLoginCookie(@Nonnull final String[] tokens, @Nonnull final HttpServletRequest request, @Nonnull final HttpServletResponse response) { final List<GrantedAuthority> authorities = Arrays.asList(tokens).subList(1, tokens.length).stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); return new User(tokens[0], "", authorities); } /** * Gets the JWT expiration time in seconds. This is usually the same as the max age for the cookie. * * @return the expiration time in seconds */ private int getExpirationTimeSeconds() { final int tokenValiditySeconds = getTokenValiditySeconds(); return (tokenValiditySeconds >= 0) ? tokenValiditySeconds : TWO_WEEKS_S; } /** * Gets the secret key for the JWT signature. * * <p>The key is constructed based on which the configured signature algorithm.</p> * * @return the secret key * @throws IllegalStateException if the algorithm is not supported */ private Key getSecretKey() { if (secretKey == null) { switch (algorithmIdentifier.substring(0, 2)) { case "HS": secretKey = new HmacKey(getKey().getBytes()); break; default: throw new IllegalStateException("Not a supported JWT algorithm: " + algorithmIdentifier); } } return secretKey; } }