quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.82k stars 2.69k forks source link

smallrye-jwt not working in native mode #1163

Closed starksm64 closed 5 years ago

starksm64 commented 5 years ago

Testing the using-jwt-rbac under native mode produces no errors, but neither authentication or authorization are working.

gsmet commented 5 years ago

@starksm64 the Java agent created by @stuartwdouglas in https://github.com/jbossas/protean-shamrock/pull/1172 might help you with this.

starksm64 commented 5 years ago

Once I got some logging configuration working, at least one issue is obvious:

Caused by: org.jose4j.lang.UnresolvableKeyException: Failed to read location as any of JWK, JWKS, PEM; META-INF/resources/publicKey.pem
    at io.smallrye.jwt.auth.principal.KeyLocationResolver.resolveKey(KeyLocationResolver.java:73)
    at org.jose4j.jwt.consumer.JwtConsumer.processContext(JwtConsumer.java:205)
    ... 48 more

I thought the META-INF/resources directory would be automatically included in native images, but apparently not.

starksm64 commented 5 years ago

Once the resource is included, the next failure is due to:

2019-03-04 22:03:15,352 DEBUG [io.sma.jwt.aut.pri.KeyLocationResolver] (XNIO-1 task-1) Failed to read location as PEM: java.security.NoSuchAlgorithmException: RSA KeyFactory not available
    at java.security.KeyFactory.<init>(KeyFactory.java:138)
    at java.security.KeyFactory.getInstance(KeyFactory.java:172)
    at io.smallrye.jwt.KeyUtils.decodePublicKey(KeyUtils.java:133)
    at io.smallrye.jwt.auth.principal.KeyLocationResolver.tryAsPEM(KeyLocationResolver.java:81)
    at io.smallrye.jwt.auth.principal.KeyLocationResolver.resolveKey(KeyLocationResolver.java:70)
    at org.jose4j.jwt.consumer.JwtConsumer.processContext(JwtConsumer.java:205)
....

Which is due to: https://github.com/oracle/graal/blob/master/substratevm/JCA-SECURITY-SERVICES.md

For the smallrye-jwt extension, the ideal solution would be what they suggest at the end of the document with regard to Alternative to --enable-all-security-services:

Registering all security services doesn't come for free. The additional code increases the native image size. If your application only requires a subset of the security services you can manually register the corresponding classes for reflection and push the initialization of some seed generators to runtime. However this requires deep knowledge of the JCA architecture. We are investigating the posibility to provide a finer grain declarative configuration of security services for future releases. If you want to take on this task youreslf you can start by reading the com.oracle.svm.hosted.SecurityServicesFeature class. This is where most of the code behind the --enable-all-security-services option is implemented.

dmlloyd commented 5 years ago

We need some smart solution for security services. I was thinking it might be good to have a Feature which transforms calls like KeyFactory.getInstance("Blah") into direct constructor calls. That would at least cover the case where there's a fixed algorithm name (which is pretty common AFAICT).

Another possible solution would be to just @Substitute these calls, and turn it into a fixed string-based if/else tree. Then we could introduce a Feature that allows constant-folding for string comparison (something that's been on my list for a while now). This would effectively do the same thing.

The underlying theme would be to allow zero-configuration support for adding security services, without blowing up the image size if few or none are actually used (which is a negative for registering everything in sight for reflection).

starksm64 commented 5 years ago

Right now I am going through each JCE factory class replacement to see what it will take to get the smallrye-jwt extension working. It was a simple replacement to get the RSA KeyFactory working, now java.security.Signature needs a replacement:

Caused by: org.jose4j.lang.InvalidKeyException: The given key (algorithm=RSA) is not valid for SHA256withRSA
    at org.jose4j.jws.BaseSignatureAlgorithm.initForVerify(BaseSignatureAlgorithm.java:115)
    at org.jose4j.jws.BaseSignatureAlgorithm.verifySignature(BaseSignatureAlgorithm.java:56)
    at org.jose4j.jws.JsonWebSignature.verifySignature(JsonWebSignature.java:192)
    at org.jose4j.jwt.consumer.JwtConsumer.processContext(JwtConsumer.java:214)
    ... 48 more
Caused by: java.security.InvalidKeyException: No installed provider supports this key: sun.security.rsa.RSAPublicKeyImpl
    at java.security.Signature$Delegate.chooseProvider(Signature.java:1138)
    at java.security.Signature$Delegate.engineInitVerify(Signature.java:1170)
    at java.security.Signature.initVerify(Signature.java:460)
    at org.jose4j.jws.BaseSignatureAlgorithm.initForVerify(BaseSignatureAlgorithm.java:111)
    ... 51 more
starksm64 commented 5 years ago

So these two replacement classes:

package io.quarkus.elytron.security.runtime.graal;

import java.security.NoSuchAlgorithmException;
import java.security.Signature;

import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;

import sun.security.rsa.SunRsaSign;

@TargetClass(Signature.class)
public final class Target_java_security_Signature {

    @Substitute
    public static Signature getInstance(String algorithm) throws NoSuchAlgorithmException {
        if (algorithm.endsWith("RSA")) {
            SunRsaSign provider = new SunRsaSign();
            return Signature.getInstance(algorithm, provider);
        }
        throw new NoSuchAlgorithmException(algorithm);
    }
}
package io.quarkus.elytron.security.runtime.graal;

import java.security.KeyFactory;
import java.security.KeyFactorySpi;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;

import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;

import sun.security.rsa.RSAKeyFactory;
import sun.security.rsa.SunRsaSign;

@TargetClass(KeyFactory.class)
public final class Target_java_security_KeyFactory {
    static class QuarkusKeyFactory extends KeyFactory {
        QuarkusKeyFactory(KeyFactorySpi keyFacSpi, Provider provider, String algorithm) {
            super(keyFacSpi, provider, algorithm);
        }
    }

    @Substitute
    public static KeyFactory getInstance(String algorithm) throws NoSuchAlgorithmException {
        if (algorithm.equals("RSA")) {
            SunRsaSign rsaProvider = new SunRsaSign();
            KeyFactorySpi keyFactorySpi = new RSAKeyFactory();
            return new QuarkusKeyFactory(keyFactorySpi, rsaProvider, algorithm);
        }
        return null;
    }
}

and this update to the SecurityDeploymentProcessor:

    @BuildStep
    void services(BuildProducer<ReflectiveClassBuildItem> classes) {
        String[] allClasses = {
                "org.wildfly.security.password.impl.PasswordFactorySpiImpl",
                "sun.security.rsa.RSASignature$MD2withRSA",
                "sun.security.rsa.RSASignature$MD5withRSA",
                "sun.security.rsa.RSASignature$SHA1withRSA",
                "sun.security.rsa.RSASignature$SHA224withRSA",
                "sun.security.rsa.RSASignature$SHA256withRSA",
                "sun.security.rsa.RSASignature$SHA384withRSA",
                "sun.security.rsa.RSASignature$SHA512withRSA"
        };
        classes.produce(new ReflectiveClassBuildItem(false, false, allClasses));
    }

have the smallrye-jwt extension working in native mode.

While simple, these are hardcoded and specific to the sun providers. I'll look further into just trying to load a named java.security.Provider and register its associated classes for reflection as that is essentially what the com.oracle.svm.hosted.SecurityServicesFeature does for all providers.

sebastienblanc commented 5 years ago

@starksm64 Hi Scott ! Is there any (even dirty :) ) workaround for this right now ? I would like to demo this in 10 days at a conference and it would be nice if I could show the native image working as well. Thx !

starksm64 commented 5 years ago

I'll push my local changes to my quarkus fork so that you could take and build that for a demo in the interim. I'll post another comment here when it is pushed.

starksm64 commented 5 years ago

@sebastienblanc I have pushed my changes to a new public repo on the iss1163 branch: https://github.com/starksm64/quarkus/tree/iss1163

With that, the using-jwt-rbac quickstart integration tests run under native mode, although there are a couple of fixes in that as well. One is a bug in the testHelloDenyAll() test and a wrong annotation on the associated endpoint.

The other is that in native mode, the LottoNumbersResource is not being introspected correctly due to what looks like a bug in the resteasy extension, but can be worked around by adding the @RegisterForReflection annotation to the class:

import io.quarkus.runtime.annotations.RegisterForReflection;
import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;

@RegisterForReflection
@Dependent
public class LottoNumbersResource {
    @Inject
    JsonWebToken jwt;
    @Inject
    @Claim(standard = Claims.birthdate)
    Optional<JsonString> birthdate;
...
sebastienblanc commented 5 years ago

@starksm64 Thanks ! I will give it a try tomorrow !

sebastienblanc commented 5 years ago

@starksm64 Works perfectly !

sebastienblanc commented 5 years ago

Is this issue planned to be fixed in 0.13 ?

starksm64 commented 5 years ago

Yes, either via the workaround that I have or via the more general provider registration that I'm testing.