/** * This file is part of git-as-svn. It is subject to the license terms * in the LICENSE file found in the top-level directory of this distribution * and at http://www.gnu.org/licenses/gpl-2.0.html. No part of git-as-svn, * including this file, may be copied, modified, propagated, or distributed * except according to the terms contained in the LICENSE file. */ package svnserver.ext.web.token; import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Hex; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jose4j.jwe.JsonWebEncryption; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.MalformedClaimException; import org.jose4j.jwt.NumericDate; import org.jose4j.jwt.consumer.InvalidJwtException; import org.jose4j.lang.JoseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import svnserver.HashHelper; import svnserver.auth.User; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.regex.Pattern; /** * Helper for working with authentication tokens. * * @author Artem V. Navrotskiy <bozaro@users.noreply.github.com> */ public class TokenHelper { @NotNull private static final Logger log = LoggerFactory.getLogger(TokenHelper.class); @NotNull private static final Pattern hex = Pattern.compile("^[0-9a-fA-F]+$"); @NotNull public static String createToken(@NotNull JsonWebEncryption jwe, @NotNull User user, @NotNull NumericDate expireAt) { try { JwtClaims claims = new JwtClaims(); claims.setExpirationTime(expireAt); claims.setGeneratedJwtId(); // a unique identifier for the token claims.setIssuedAtToNow(); // when the token was issued/created (now) claims.setNotBeforeMinutesInThePast(0.5f); // time before which the token is not yet valid (30 seconds ago) if (!user.isAnonymous()) { claims.setSubject(user.getUserName()); // the subject/principal is whom the token is about setClaim(claims, "email", user.getEmail()); setClaim(claims, "name", user.getRealName()); setClaim(claims, "external", user.getExternalId()); } jwe.setPayload(claims.toJson()); return jwe.getCompactSerialization(); } catch (JoseException e) { throw new IllegalStateException(e); } } @Nullable public static User parseToken(@NotNull JsonWebEncryption jwe, @NotNull String token, int tokenEnsureTime) { try { jwe.setCompactSerialization(token); final JwtClaims claims = JwtClaims.parse(jwe.getPayload()); final NumericDate now = NumericDate.now(); final NumericDate expire = NumericDate.fromMilliseconds(now.getValueInMillis()); if (tokenEnsureTime > 0) { expire.addSeconds(tokenEnsureTime); } if (claims.getExpirationTime() == null || claims.getExpirationTime().isBefore(expire)) { return null; } if (claims.getNotBefore() == null || claims.getNotBefore().isAfter(now)) { return null; } if (claims.getSubject() == null) { return User.getAnonymous(); } return User.create( claims.getSubject(), claims.getClaimValue("name", String.class), claims.getClaimValue("email", String.class), claims.getClaimValue("external", String.class) ); } catch (JoseException | MalformedClaimException | InvalidJwtException e) { log.warn("Token parsing error: " + e.getMessage()); return null; } } @NotNull public static byte[] secretToBytes(@NotNull String secret, int length) { try { if (secret.length() == length * 2 && hex.matcher(secret).find()) { return Hex.decodeHex(secret.toCharArray()); } final byte[] hash = HashHelper.sha256().digest(secret.getBytes(StandardCharsets.UTF_8)); return Arrays.copyOf(hash, length); } catch (DecoderException e) { throw new IllegalStateException(e); } } private static void setClaim(JwtClaims claims, @NotNull String name, @Nullable Object value) { if (value != null) { claims.setClaim(name, value); } } }