package io.cattle.platform.token.impl;
import io.cattle.platform.archaius.util.ArchaiusUtil;
import io.cattle.platform.token.TokenDecryptionException;
import io.cattle.platform.token.TokenException;
import io.cattle.platform.token.TokenService;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.Date;
import java.util.Map;
import javax.inject.Inject;
import org.apache.commons.lang.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import com.netflix.config.DynamicLongProperty;
import com.netflix.config.DynamicStringProperty;
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObject;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.jca.JCASupport;
import com.nimbusds.jose.crypto.RSADecrypter;
import com.nimbusds.jose.crypto.RSAEncrypter;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jwt.EncryptedJWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
public class JwtTokenServiceImpl implements TokenService {
private static final String KEY_ID = "kid";
private static final DynamicStringProperty SUBJECT = ArchaiusUtil.getString("jwt.default.subject");
private static final DynamicStringProperty ISSUER = ArchaiusUtil.getString("jwt.default.issuer");
private static final DynamicLongProperty EXPIRATION = ArchaiusUtil.getLong("jwt.default.expiration.seconds");
RSAKeyProvider keyProvider;
// Delete after Java 8 switch
static {
if (!JCASupport.isSupported(EncryptionMethod.A128GCM) &&
java.security.Security.getProvider("BC") == null) {
java.security.Security.addProvider(new BouncyCastleProvider());
}
}
@Override
public String generateToken(Map<String, Object> payload) {
return generateToken(payload, new Date(), new Date(System.currentTimeMillis() + EXPIRATION.get() * 1000), false);
}
@Override
public String generateToken(Map<String, Object> payload, Date expireDate) {
return generateToken(payload, new Date(), expireDate, false);
}
@Override
public String generateEncryptedToken(Map<String, Object> payload) {
return generateToken(payload, new Date(), new Date(System.currentTimeMillis() + EXPIRATION.get() * 1000), true);
}
@Override
public String generateEncryptedToken(Map<String, Object> payload, Date expireDate) {
return generateToken(payload, new Date(), expireDate, true);
}
protected String generateToken(Map<String, Object> payload, Date issueTime, Date expireDate, boolean encrypted) {
// Prepare JWT with claims set
JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
if (payload != null) {
for (Map.Entry<String, Object> entry : payload.entrySet()) {
builder.claim(entry.getKey(), entry.getValue());
}
}
if (expireDate != null) {
builder.expirationTime(expireDate);
}
builder.subject(SUBJECT.get());
builder.issueTime(issueTime);
builder.issuer(ISSUER.get());
if (encrypted) {
JWEHeader header = new JWEHeader(JWEAlgorithm.RSA_OAEP, EncryptionMethod.A128GCM);
EncryptedJWT jwt = new EncryptedJWT(header, builder.build());
RSAEncrypter encrypter = new RSAEncrypter((RSAPublicKey) keyProvider.getDefaultPublicKey());
try {
jwt.encrypt(encrypter);
} catch (JOSEException e) {
throw new RuntimeException("Failed to generate encrypted token", e);
}
return jwt.serialize();
} else {
RSAPrivateKeyHolder privateKey = keyProvider.getPrivateKey();
builder.claim(KEY_ID, privateKey.getKeyId());
// Create RSA-signer with the private key
JWSSigner signer = new RSASSASigner(privateKey.getKey());
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), builder.build());
// Compute the RSA signature
try {
signedJWT.sign(signer);
} catch (JOSEException e) {
throw new RuntimeException("Failed to generate token", e);
}
return signedJWT.serialize();
}
}
public RSAKeyProvider getKeyProvider() {
return keyProvider;
}
@Override
public Map<String, Object> getJsonPayload(String token, boolean encrypted) throws TokenException {
if (StringUtils.isEmpty(token)) {
throw new TokenException("null or empty token");
}
if (encrypted) {
EncryptedJWT jwt = null;
try {
jwt = EncryptedJWT.parse(token);
RSADecrypter decrypter = new RSADecrypter(keyProvider.getPrivateKey().getKey());
jwt.decrypt(decrypter);
} catch (JOSEException | ParseException e) {
throw new TokenDecryptionException("Invalid token", e);
}
return getJSONObject(jwt, encrypted);
}
try {
JWSObject jws = JWSObject.parse(token);
JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) keyProvider.getDefaultPublicKey());
if (!jws.verify(verifier)) {
throw new TokenException("ERROR: Fradulent token");
}
return getJSONObject(jws, encrypted);
} catch (TokenException | ParseException | JOSEException e) {
throw new TokenException("Error: Fradulent token, unrecognized signature", e);
}
}
private Map<String, Object> getJSONObject(JOSEObject jose, boolean encrypted) throws TokenException {
Long exp = (Long) jose.getPayload().toJSONObject().get("exp");
if (exp != null && exp * 1000 <= System.currentTimeMillis()) {
throw new TokenException("Expired Token");
}
return jose.getPayload().toJSONObject();
}
@Inject
public void setKeyProvider(RSAKeyProvider keyProvider) {
this.keyProvider = keyProvider;
}
}