Friday, August 31. 2018
Comparing rhino and nashorn Java JS engines
A quick entry this time. Some days ago I needed to use the javascript engine in Java (javascript in Java is integrated inside the Scripting API). I realized that the new nashorn engine always compiles the code and, although it is much faster and modern than the older one (rhino was used in previous java 7), in some corner cases it can be worse. Normally in cases where a lot of different javascript chunks are evaluated. The rhino implementation interprets the code and just optimizes the hot parts while nashorn always compiles the code into classes and, therefore, if the piece of javascript is just run a few times the compiling part is a drawback. Bug JDK-8034959 manages this same subject.
In theory the old rhino implementation can be added to the Scripting API. But the problem is that now all java.dev.net is decommissioned and finding the sources is complicated currently. The following steps are needed now.
Download last rhino library 1.7R4. This provides the js.jar library.
Now we have to find the rhino implementation for JSR-223 (Scripting API), and I finally found a github clone, and I could compile it.
git clone https://github.com/scijava/javax-scripting cd javax-scripting/engines/javascript/lib/ cp ${RHINO_HOME}/rhino1_7R4/js.jar . # downloaded in step 1 cd ../make/ ant clean all
A library js-engine.jar is generated inside the build directory.
This little program is used to test. As you see I have to use the rhino engine name (because nashorn is also there).
import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; public class HelloWorld { public static void main(String[] args) throws ScriptException { ScriptEngine engine = new ScriptEngineManager().getEngineByName("rhino"); System.out.println(engine.getClass()); engine.eval("print('Hello World!');"); } }
With both libraries the simple javascript can be compiled and executed.
java -cp js.jar:js-engine.jar:. HelloWorld class com.sun.phobos.script.javascript.RhinoScriptEngine Hello World!
To completely remove the nashorn engine in JDK 8 it is necessary to remove the library from the JVM. The library is placed in ${JAVA_HOME}/jre/lib/ext/nashorn.jar (it is placed in the ext folder, and all the libraries in that folder are automatically available for the JDK, this way nashorn is always loaded if not deleted). Once it is removed only the rhino engine is available for names like js or JavaScript or mime types like application/javascript.
But, remember this is the last resort. The new nashorn engine is much faster and modern. The js snippets are cached to avoid re-compilation so it's only a problem if the code is really very dynamic (I suppose that you can also maintain the same engine, taking care of the javascript, global vars and global execution). Just to compare both engines I tried with the google octane benchmark (I used this files working a bit over the run-octane.js JDK script). The results are the following for both engines in my laptop.
rhino | nashorn | |||||
---|---|---|---|---|---|---|
test | warmup | avg | max | warmup | avg | max |
box2d | 112 | 195 | 233 | 30 | 474 | 1343 |
code-load | 10164 | 12818 | 13320 | 120 | 152 | 159 |
crypto | N/A | N/A | N/A | 861 | 2664 | 2830 |
deltablue | 6491 | 6822 | 6873 | 28080 | 62742 | 65376 |
earley-boyer | 503 | 519 | 523 | 324 | 4911 | 5581 |
gbemu | N/A | N/A | N/A | 8 | 232 | 325 |
mandreel | N/A | N/A | N/A | 5 | 45 | 61 |
navier-stokes | 351 | 400 | 404 | 738 | 1013 | 1026 |
pdfjs | N/A | N/A | N/A | 11 | 416 | 516 |
raytrace | 766 | 792 | 804 | 681 | 10502 | 11093 |
regexp | 206 | 216 | 225 | 520 | 685 | 731 |
richards | 10124 | 10455 | 10776 | 46848 | 67724 | 68916 |
splay | 17376 | 15632 | 17141 | 45639 | 53368 | 60664 |
typescript | 6 | 10 | 10 | 1 | 13 | 21 |
So it's clear that nashorn is much better, faster and it can execute the whole benchmark (rhino fails in some tests). If you see there are several tests in which the old engine is better during the warmup, but all tests except code-load are faster when executed by nashorn in average and maximum (and code-load can be related to compilation in the end, because I think that it is a test about loading code).
Scripted regards!
Saturday, August 4. 2018
Authenticating a JWT token in wildfly
Sometimes I get surprised with the lack of information and/or documentation around wildfly and JBoss. If you search over the security documentation for wildfly, it supports several authentication mechanisms, by default BASIC, FORM, SPNEGO and CLIENT-CERT. There is no mention to bearer tokens or JWT but, surprise, surprise, the support is there and it can be used out of the box. There is very scarce documentation but you can find some examples and a quickstart about this feature.
A JSON Web Token (JWT) is an access token than contains three parts: a header, a payload and a signature. The header is just a JSON with some fixed fields (like type -jwt in this case-, algorithm of the signature or the id of the key used). The payload is the interesting part and is just any JSON with the information that the token is going to interchange, it can be any information, but things like username, roles, groups, name and surname are common. Among those fields there are two typical keys: iss or issuer and aud or audience. The issuer is who is issuing the token. The audience is for what endpoint the token was created. Wildfly implementation can be configured to check these two fields. Finally a signature of the header and payload is added to ensure that the token is valid and has not been modified (non-repudiation and integrity). In current days these tokens are ubiquitous to authenticate and authorize web services applications.
I had no idea about this feature (even more, I thought that there was no bearer authentication by default in wildfly) and I had recommended custom developments several times for this use-case. That was a wrong advise or, at least, an incomplete one. So, once I have heard about this mechanism I thouhgt that it was fair to test it and know more about how it works. This entry is going to test the new BEARER_TOKEN mechanism introduced by elytron in a wildfly 13. The JWT token is going to be provided by the keycloak project but the server is going to consume it just using elytron (no agent provided by keycloak is going to be used, just plain wildfly BEARER_TOKEN mechanism).
The steps to setup the demo are the following.
First download and install wildfly 13.
unzip wildfly-13.0.0.Final.zip cd wildfly-13.0.0.Final/bin/ ./add-user.sh -u admin -p XXXXX ./standalone.sh
Configure with offset 10000 (keycloak will be running at default ports):
./jboss-cli.sh --connect /socket-binding-group=standard-sockets:write-attribute(name=port-offset, value="${jboss.socket.binding.port-offset:10000}") reload
Now just do the same for keycloak.
unzip keycloak-4.1.0.Final.zip cd keycloak-4.1.0.Final/bin/ ./add-user.sh -u admin -p XXXXX ./add-user-keycloak.sh -r master -u admin -p XXXXX ./standalone.sh
Using the keycloak administration several objects are created. First a role that will be assigned to our users. In Roles click Add Role and create a user role.
Then create the user to be used with that role. Go to Users and Add user.
Assign a password in the Credentials tab (non-temporary) and finally assign the user role to it.
In Clients click Create and fill the wildlfy client information. This client will be used to get the token for the application (using direct login or direct grants) and with that token the wildfly endpoint will be called.
When you configure a client as confidential in the second tab, Credentials, you can set a secret for the client. This password will be needed for some operations later on (for login using client credentials and to configure introspection).
In the Scope tab full scope is removed and only the role user is assigned. This way the JWT token is restricted to contain only this role (if the user has it) and not all the user information.
Finally a mapper is going to be configured. This part is the only tricky point of the demo. The wildfly authenticator expects the roles in a top-level field in the JWT token, but keycloak uses the claim realm_access.roles (two levels). For that reason the role list is going to be moved to a Roles field (in elytron this is the default attribute is for roles, although any another attribute can be configured using a role-decoder). Create protocol mapper in the Mappers tab.
Now that keycloak is configured, we need the certificate that is going to be used to sign the token. We can get it from keycloak in Realm Settings → Keys → RSA type → Click Certificate. Create a keystore with it. The base64 data should be enclosed with -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- lines to be in a PEM format (the file keycloak.pem was generated).
cd ${WILDFLY_HOME}/standalone/configuration keytool -importcert -keystore keycloak.jks -storepass changeit -file keycloak.pem -alias keycloak -trustcacerts
Going back to wildfly, elytron should be configured now to create a new domain using a token-realm. In a first step the realm is going to be configured to check a jwt token.
/subsystem=elytron/key-store=jwt-key-store:add(type="JKS", relative-to=jboss.server.config.dir, path="keycloak.jks", credential-reference={clear-text="changeit"}) /subsystem=elytron/token-realm=jwt-realm:add(jwt={issuer=["http://localhost:8080/auth/realms/master"], audience=["wildfly"], key-store=jwt-key-store, certificate="keycloak"}, principal-claim="preferred_username") /subsystem=elytron/security-domain=jwt-domain:add(realms=[{realm=jwt-realm}], permission-mapper=default-permission-mapper, default-realm=jwt-realm) /subsystem=elytron/http-authentication-factory=jwt-http-authentication:add(security-domain=jwt-domain, http-server-mechanism-factory=global, mechanism-configurations=[{mechanism-name="BEARER_TOKEN", mechanism-realm-configurations=[{realm-name="jwt-realm"}]}]) /subsystem=undertow/application-security-domain=jwt-domain:add(http-authentication-factory=jwt-http-authentication)
As commented before the jwt is configured to only admit access tokens that are emitted by a set of issuers and to a specific audience (it seems those two fields are optional and can be omitted). The key-store is used to reference the certificate that keycloak is going to use to sign the tokens. This way elytron will validate that the signature is valid, being sure that the token was generated by keycloak and not modified by someone else. The attribute to identify the user will be preferred_username and nothing is specified for roles, so the default Roles attribute will be used.
Finally some debug levels are configured to check that everything is working.
/subsystem=logging/logger=org.wildfly.security:add(level=ALL) /subsystem=logging/logger=org.wildfly.elytron:add(level=ALL)
A hello-world application is developed to use jaxrs and return the name of the logged user. The endpoint is as simple as this one.
package es.rickyepoderi.sample; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; @Path("hello") public class HelloWorld { @Context private HttpServletRequest request; @GET @Produces(MediaType.TEXT_PLAIN) public String world() { return "Hello " + (request.getRemoteUser() != null? request.getRemoteUser() : "world") + "!!!"; } }
In order to use the previous configuration the web.xml should use a security-constraint and the authentication method should be set to BEARER_TOKEN. Besides the user role is configured to give access to the application (this way the application is ensuring that token roles are parsed correctly).
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <security-constraint> <web-resource-collection> <web-resource-name>Protect all application</web-resource-name> <url-pattern>/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>user</role-name> </auth-constraint> </security-constraint> <login-config> <auth-method>BEARER_TOKEN>/auth-method> <realm-name>jwt-domain</realm-name> </login-config> <security-role> <description>Role required to log in to the Application>/description> <role-name>user</role-name> </security-role> <session-config> <session-timeout>30</session-timeout> </session-config> </web-app>
In the jboss-web.xml just the domain added to undertow is configured.
<?xml version="1.0" encoding="UTF-8"?> <jboss-web> <security-domain>jwt-domain</security-domain> </jboss-web>
Now a little test.sh is created in order to test the endpoint (you can find it inside the maven project). A curl command is executed to login and get a token using direct grants over the keycloak client. That token is sent inside the Authorization header to call the wildfly endpoint. The script is very simple but it works.
bash test.sh Hello ricky!!!
The hello endpoint is returning my username so the token is being correctly validated and the username and roles are obtained.
Now let's try the introspection. The introspect is an endpoint that an OAuth 2.0 server gives to validate access tokens. This way, instead of using wildfly internal code, the container will call this endpoint to validate the access token. This has some advantages, for example, that the token is not restricted to be a JWT one.
So now the configuration of the jwt-realm should be modified from jwt to oauth2-introspect. The elytron configuration just offers to use a username/password configuration using BASIC authentication, but, as I commented previously, this is the default authentication for a confidential keycloak client, so it should work. The username is the client name, wildfly, and the password the one generated in the Credentials tab when the client was configured.
/subsystem=elytron/token-realm=jwt-realm:undefine-attribute(name=jwt) /subsystem=elytron/token-realm=jwt-realm:write-attribute(name=oauth2-introspection, value={introspection-url=http://localhost:8080/auth/realms/master/protocol/openid-connect/token/introspect, client-id=wildfly, client-secret=39917c0f-139a-42d2-af03-2b4207758242})
And my little script continues returning the same output, therefore the introspection endpoint is validating the ticket as expected.
So, it is working. Wildfly and the new elytron subsystem has a poor documented mechanism called BEARER_TOKEN, which lets you authenticate using JWT tokens (and, developing a custom realm, can be extended to more tokens, in general, to any token sent in the Authorization header with the BEARER prefix). In this entry the keycloak single sign-on product was installed to obtain the JWT token and then a token realm was configured inside elytron to validate it. First an internal validation was done (the wildfly code checks issuer, audience and signature) and then an OAuth introspection endpoint was configured to do the same (the server just calls the endpoint to validate the token). Both configuration work. Remember that the only trick was moving the roles to first level claim (Roles in this case), that is needed because wildfly just considers attributes at first level (and keycloak adds roles by default in a more complex JSON document). After my warning some documentation is starting to come to the project.
Tokenized regards!
Comments