Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PAYARA-2853 Microprofile JWT Auth v1.1 impl #3053

Merged
merged 2 commits into from
Aug 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,14 @@
*/
package fish.payara.microprofile.jwtauth.cdi;

import static org.eclipse.microprofile.jwt.Claims.UNKNOWN;

import fish.payara.microprofile.jwtauth.eesecurity.JWTAuthenticationMechanism;
import fish.payara.microprofile.jwtauth.eesecurity.SignedJWTIdentityStore;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.SessionScoped;
Expand All @@ -64,12 +63,13 @@
import javax.enterprise.inject.spi.ProcessInjectionTarget;
import javax.enterprise.inject.spi.ProcessManagedBean;
import javax.enterprise.inject.spi.ProcessSessionBean;

import org.eclipse.microprofile.auth.LoginConfig;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.jwt.Claim;

import fish.payara.microprofile.jwtauth.eesecurity.JWTAuthenticationMechanism;
import fish.payara.microprofile.jwtauth.eesecurity.SignedJWTIdentityStore;
import static org.eclipse.microprofile.jwt.Claims.UNKNOWN;
import static org.eclipse.microprofile.jwt.config.Names.VERIFIER_PUBLIC_KEY;
import static org.eclipse.microprofile.jwt.config.Names.VERIFIER_PUBLIC_KEY_LOCATION;

/**
* This CDI extension installs the {@link JWTAuthenticationMechanism} and related {@link SignedJWTIdentityStore}
Expand Down Expand Up @@ -134,7 +134,7 @@ public <T> void findRoles(@Observes ProcessManagedBean<T> eventIn, BeanManager b
}

public <T> void checkInjectIntoRightScope(@Observes ProcessInjectionTarget<T> eventIn, BeanManager beanManager) {

ProcessInjectionTarget<T> event = eventIn; // JDK8 u60 workaround

for (InjectionPoint injectionPoint : event.getInjectionTarget().getInjectionPoints()) {
Expand All @@ -157,7 +157,9 @@ public <T> void checkInjectIntoRightScope(@Observes ProcessInjectionTarget<T> ev
"Claim value " + claim.value() + " should be equal to claim standard " + claim.standard().name() +
" or one of those should be left at their default value");
}

}

}
}

Expand All @@ -166,9 +168,20 @@ public void installMechanismIfNeeded(@Observes AfterBeanDiscovery eventIn, BeanM
AfterBeanDiscovery afterBeanDiscovery = eventIn; // JDK8 u60 workaround

if (addJWTAuthenticationMechanism) {
validateConfigValue();
CdiInitEventHandler.installAuthenticationMechanism(afterBeanDiscovery);
}
}

private void validateConfigValue() {
Config config = ConfigProvider.getConfig();
if (config.getOptionalValue(VERIFIER_PUBLIC_KEY, String.class).isPresent()
&& config.getOptionalValue(VERIFIER_PUBLIC_KEY_LOCATION, String.class).isPresent()) {
throw new DeploymentException(
"Both properties mp.jwt.verify.publickey and mp.jwt.verify.publickey.location must not be defined"
);
}
}

public Set<String> getRoles() {
return roles;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,40 @@
*/
package fish.payara.microprofile.jwtauth.eesecurity;

import static java.lang.Thread.currentThread;
import static java.util.logging.Level.FINEST;
import static javax.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT;

import fish.payara.microprofile.jwtauth.jwt.JsonWebTokenImpl;
import fish.payara.microprofile.jwtauth.jwt.JwtTokenParser;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import static java.lang.Thread.currentThread;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import static java.util.logging.Level.FINEST;
import java.util.logging.Logger;

import javax.enterprise.inject.spi.DeploymentException;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.security.enterprise.identitystore.CredentialValidationResult;
import static javax.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT;
import javax.security.enterprise.identitystore.IdentityStore;

import fish.payara.microprofile.jwtauth.jwt.JsonWebTokenImpl;
import fish.payara.microprofile.jwtauth.jwt.JwtTokenParser;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import static org.eclipse.microprofile.jwt.config.Names.ISSUER;
import static org.eclipse.microprofile.jwt.config.Names.VERIFIER_PUBLIC_KEY;
import static org.eclipse.microprofile.jwt.config.Names.VERIFIER_PUBLIC_KEY_LOCATION;

/**
* Identity store capable of asserting that a signed JWT token is valid according to
Expand All @@ -68,60 +82,176 @@
*/
public class SignedJWTIdentityStore implements IdentityStore {

private static final Logger logger = Logger.getLogger(SignedJWTIdentityStore.class.getName());

private static final Logger LOGGER = Logger.getLogger(SignedJWTIdentityStore.class.getName());

private static final String RSA_ALGORITHM = "RSA";

private final JwtTokenParser jwtTokenParser = new JwtTokenParser();
private String acceptedIssuer;

private final String acceptedIssuer;

private final Config config;

public SignedJWTIdentityStore() {
try {
Properties properties = new Properties();
properties.load(currentThread().getContextClassLoader().getResource("/payara-mp-jwt.properties").openStream());

acceptedIssuer = properties.getProperty("accepted.issuer");

} catch (IOException e) {
throw new IllegalStateException("Failed to load properties", e);
}
config = ConfigProvider.getConfig();
acceptedIssuer = readVendorIssuer()
.orElseGet(() -> config.getOptionalValue(ISSUER, String.class)
.orElseThrow(() -> new IllegalStateException("No issuer found")));
}

public CredentialValidationResult validate(SignedJWTCredential signedJWTCredential) {
try {
JsonWebTokenImpl jsonWebToken =
jwtTokenParser.parse(
signedJWTCredential.getSignedJWT(),
acceptedIssuer,
readPublicKey("/publicKey.pem"));

Optional<PublicKey> publicKey = readPublicKeyFromLocation("/publicKey.pem");
if (!publicKey.isPresent()) {
publicKey = readMPEmbeddedPublicKey();
}
if (!publicKey.isPresent()) {
publicKey = readMPPublicKeyFromLocation();
}
if (!publicKey.isPresent()) {
throw new IllegalStateException("No PublicKey found");
}

JsonWebTokenImpl jsonWebToken
= jwtTokenParser.parse(
signedJWTCredential.getSignedJWT(),
acceptedIssuer,
publicKey.get()
);

List<String> groups = new ArrayList<String>(
List<String> groups = new ArrayList<>(
jsonWebToken.getClaim("groups"));

return new CredentialValidationResult(
jsonWebToken,
new HashSet<>(groups));

} catch (Exception e) {
logger.log(FINEST, "Exception trying to parse JWT token.", e);
LOGGER.log(FINEST, "Exception trying to parse JWT token.", e);
}

return INVALID_RESULT;
}

public PublicKey readPublicKey(String resourceName) throws Exception {

private Optional<String> readVendorIssuer() {
String issuer = null;
URL mpJwtResource = currentThread().getContextClassLoader().getResource("/payara-mp-jwt.properties");
if (mpJwtResource != null) {
try {
Properties properties = new Properties();
properties.load(mpJwtResource.openStream());
issuer = properties.getProperty("accepted.issuer");
} catch (IOException e) {
throw new IllegalStateException("Failed to load properties", e);
}
}
return Optional.ofNullable(issuer);
}

private Optional<PublicKey> readMPEmbeddedPublicKey() throws Exception {
Optional<String> key = config.getOptionalValue(VERIFIER_PUBLIC_KEY, String.class);
if (!key.isPresent()) {
return Optional.empty();
}
return createPublicKey(key.get());
}

private Optional<PublicKey> readMPPublicKeyFromLocation() throws Exception {
Optional<String> locationOpt = config.getOptionalValue(VERIFIER_PUBLIC_KEY_LOCATION, String.class);

if (!locationOpt.isPresent()) {
return Optional.empty();
}

String publicKeyLocation = locationOpt.get();

return readPublicKeyFromLocation(publicKeyLocation);
}

private Optional<PublicKey> readPublicKeyFromLocation(String publicKeyLocation) throws Exception {

URL publicKeyURL = currentThread().getContextClassLoader().getResource(publicKeyLocation);

if (publicKeyURL == null) {
try {
publicKeyURL = new URL(publicKeyLocation);
} catch (MalformedURLException ex) {
publicKeyURL = null;
}
}
if (publicKeyURL == null) {
return Optional.empty();
}

byte[] byteBuffer = new byte[16384];
int length = currentThread().getContextClassLoader()
.getResource(resourceName)
.openStream()
.read(byteBuffer);

String key = new String(byteBuffer, 0, length).replaceAll("-----BEGIN (.*)-----", "")
.replaceAll("-----END (.*)----", "")
.replaceAll("\r\n", "")
.replaceAll("\n", "")
.trim();

return KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(key)));
int length = publicKeyURL.openStream()
.read(byteBuffer);
String key = new String(byteBuffer, 0, length);
return createPublicKey(key);
}


private Optional<PublicKey> createPublicKey(String key) throws Exception {
try {
return Optional.of(createPublicKeyFromPem(key));
} catch (Exception pemEx) {
try {
return Optional.of(createPublicKeyFromJWKS(key));
} catch (Exception jwksEx) {
throw new DeploymentException(jwksEx);
}
}
}

private PublicKey createPublicKeyFromPem(String key) throws Exception {
key = key.replaceAll("-----BEGIN (.*)-----", "")
.replaceAll("-----END (.*)----", "")
.replaceAll("\r\n", "")
.replaceAll("\n", "")
.trim();

byte[] keyBytes = Base64.getDecoder().decode(key);
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(keyBytes);
return KeyFactory.getInstance(RSA_ALGORITHM)
.generatePublic(publicKeySpec);

}

private PublicKey createPublicKeyFromJWKS(String jwksValue) throws Exception {
JsonObject jwks = parseJwks(jwksValue);
JsonArray keys = jwks.getJsonArray("keys");
JsonObject jwk = keys != null ? keys.getJsonObject(0) : jwks;

// the public exponent
byte[] exponentBytes = Base64.getUrlDecoder().decode(jwk.getString("e"));
BigInteger exponent = new BigInteger(1, exponentBytes);

// the modulus
byte[] modulusBytes = Base64.getUrlDecoder().decode(jwk.getString("n"));
BigInteger modulus = new BigInteger(1, modulusBytes);

RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(modulus, exponent);
return KeyFactory.getInstance(RSA_ALGORITHM)
.generatePublic(publicKeySpec);
}

private JsonObject parseJwks(String jwksValue) throws Exception {
JsonObject jwks;
try {
jwks = Json.createReader(new StringReader(jwksValue))
.readObject();
} catch (Exception ex) {
// if jwks is encoded
byte[] jwksDecodedValue = Base64.getDecoder().decode(jwksValue);
try (InputStream jwksStream = new ByteArrayInputStream(jwksDecodedValue)) {
jwks = Json.createReader(jwksStream)
.readObject();
}
}
return jwks;
}



}
2 changes: 1 addition & 1 deletion appserver/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@

<!-- Microprofile version properties -->
<microprofile-fault-tolerance.version>1.0.payara-p1</microprofile-fault-tolerance.version>
<microprofile-jwt-auth.version>1.0.payara-p1</microprofile-jwt-auth.version>
<microprofile-jwt-auth.version>1.1.payara-p1</microprofile-jwt-auth.version>
<microprofile-healthcheck.version>1.0.payara-p1</microprofile-healthcheck.version>
<microprofile-metrics.version>1.1.payara-p1</microprofile-metrics.version>
<microprofile-rest-client.version>1.1-payara-p1</microprofile-rest-client.version>
Expand Down