/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* licenses this file to you 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 the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.
*/
package org.apereo.portal.soffit.service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.UUID;
import javax.annotation.PostConstruct;
import org.apache.commons.lang3.StringUtils;
import org.apereo.portal.soffit.ITokenizable;
import org.jasypt.util.text.BasicTextEncryptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
/**
* Base class for services that produce JASON Web Tokens.
*
* @since 5.0
*/
public class AbstractJwtService {
public static final String JWT_ISSUER = "Soffit";
public static final String SIGNATURE_KEY_PROPERTY = "org.apereo.portal.soffit.jwt.signatureKey";
public static final String DEFAULT_SIGNATURE_KEY = "CHANGEME";
public static final String ENCRYPTION_PASSWORD_PROPERTY =
"org.apereo.portal.soffit.jwt.encryptionPassword";
public static final String DEFAULT_ENCRYPTION_PASSWORD = "CHANGEME";
protected final Logger logger = LoggerFactory.getLogger(getClass());
@Value("${" + SIGNATURE_KEY_PROPERTY + ":" + DEFAULT_SIGNATURE_KEY + "}")
private String signatureKey;
@Value("${" + ENCRYPTION_PASSWORD_PROPERTY + ":" + DEFAULT_ENCRYPTION_PASSWORD + "}")
private String encryptionPassword;
/*
* NOTE: There is also a StrongTextEncryptor, but it requires each deployment
* to download and install the "Java Cryptography Extension (JCE) Unlimited
* Strength Jurisdiction Policy Files," which sounds like a tremendous PITA.
* The BasicTextEncryptor supports "normal-strength encryption of texts,"
* which should be satisfactory for our needs.
*/
final BasicTextEncryptor textEncryptor = new BasicTextEncryptor();
@PostConstruct
public void init() {
// Signature Key
if (StringUtils.isBlank(signatureKey)) {
logger.error("The value of required property {} is blank", SIGNATURE_KEY_PROPERTY);
throw new IllegalStateException("Missing property " + SIGNATURE_KEY_PROPERTY);
} else if (DEFAULT_SIGNATURE_KEY.equals(signatureKey)) {
logger.warn(
"Property {} is using the deafult value; please change it",
SIGNATURE_KEY_PROPERTY);
}
// Encryption Passowrd
if (StringUtils.isBlank(encryptionPassword)) {
logger.error(
"The value of required property {} is blank", ENCRYPTION_PASSWORD_PROPERTY);
throw new IllegalStateException("Missing property " + ENCRYPTION_PASSWORD_PROPERTY);
} else if (DEFAULT_ENCRYPTION_PASSWORD.equals(encryptionPassword)) {
logger.warn(
"Property {} is using the deafult value; please change it",
ENCRYPTION_PASSWORD_PROPERTY);
}
textEncryptor.setPassword(encryptionPassword);
}
protected Claims createClaims(
Class<? extends ITokenizable> clazz, String username, Date expires) {
// Registered claims
final Claims rslt =
Jwts.claims()
.setIssuer(JWT_ISSUER)
.setSubject(username)
.setExpiration(expires)
.setIssuedAt(new Date())
.setId(UUID.randomUUID().toString());
// Deserialization class
rslt.put(JwtClaims.CLASS.getName(), clazz.getName());
return rslt;
}
protected String generateEncryptedToken(Claims claims) {
final String jwt =
Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, signatureKey)
.compact();
// Encryption
final String rslt = textEncryptor.encrypt(jwt);
return rslt;
}
protected Jws<Claims> parseEncrypteToken(
String encryptedToken, Class<? extends ITokenizable> clazz) {
// Decryption
final String jwt = textEncryptor.decrypt(encryptedToken);
final Jws<Claims> rslt = Jwts.parser().setSigningKey(signatureKey).parseClaimsJws(jwt);
// Token expired?
final Date expires = rslt.getBody().getExpiration();
if (expires.before(new Date())) {
final String msg = "The specified token is expired: " + rslt;
throw new SecurityException(msg);
}
// Sanity check
final String s = (String) rslt.getBody().get(JwtClaims.CLASS.getName());
if (!clazz.getName().equals(s)) {
// Opportunity for future versioning of the data model... needs work
String msg =
"Token class mismatch; expected '" + clazz.getName() + "' but was '" + s + "'";
throw new RuntimeException(msg);
}
return rslt;
}
}