Issue parsing JWTOptions appearing with vert.x 4.0.2

447 views
Skip to first unread message

Dominique Morel

unread,
Feb 8, 2021, 4:37:18 AM2/8/21
to vert.x
Hello vert.x community

We tried vert.x 4.0.2 last friday, it solves the thread pool Issue #3786, thank you Thomas for your quick fix.
However I have an issue now with JWT configuration..

Here is my configuration :

{
  "permissionsClaimKey": "realm_access/roles",
  "jwtOptions": {
    "ignoreExpiration": true,
    "audience": [
      "XXX"
    ],
  },
  "pubSecKeys": [
    {
      "algorithm": "ES256",
      "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMy9p7xiTEhRvaYHuA9i1T/f2Z2A6qyCbR3abUdR5G/A6TFcfoow0InVfBTcpyMFW3DBaclWgqC3piMQXEreQ8Q==",
      "buffer": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMy9p7xiTEhRvaYHuA9i1T/f2Z2A6\nqyCbR3abUdR5G/A6TFcfoow0InVfBTcpyMFW3DBaclWgqC3piMQXEreQ8Q==\n-----END PUBLIC KEY-----\n"
    }
  ]
}

and the issue I get when trying to create my JWTOptions :

JWTAuthOptions jwtAuthOptions = new JWTAuthOptions(authOptionsJson);

Illegal base64 character 20
java.lang.IllegalArgumentException: Illegal base64 character 20
at java.base/java.util.Base64$Decoder.decode0(Base64.java:743)
at java.base/java.util.Base64$Decoder.decode(Base64.java:535)
at java.base/java.util.Base64$Decoder.decode(Base64.java:558)
at io.vertx.ext.auth.PubSecKeyOptionsConverter.fromJson(PubSecKeyOptionsConverter.java:26)
at io.vertx.ext.auth.PubSecKeyOptions.<init>(PubSecKeyOptions.java:52)
at io.vertx.ext.auth.jwt.JWTAuthOptionsConverter.lambda$fromJson$1(JWTAuthOptionsConverter.java:49)
at java.base/java.lang.Iterable.forEach(Iterable.java:75)
at io.vertx.ext.auth.jwt.JWTAuthOptionsConverter.fromJson(JWTAuthOptionsConverter.java:47)
at io.vertx.ext.auth.jwt.JWTAuthOptions.<init>(JWTAuthOptions.java:77)

What I understand : it tries to decode public key in base64 format, but does not get of the PUBLIC/END PUBLIC KEY tags before, so it breaks because of the space (0x20) character after PUBLIC. 
I did not face this issue using vert.x 4.0.0 milestone.
I tried to workaround by removing tags, but without success (I get errors either "java.lang.IllegalArgumentException: PEM contains not enough lines" or  "Illegal base64 character a".
Thanks for your help

Dominique


Jonad García San Martín

unread,
Feb 8, 2021, 8:56:00 AM2/8/21
to vert.x
Hello Dominique,

I am new in the group and I am relatively junior. But I see a difference between the key inside "publicKey" and the key inside "buffer". In the second one there is one break line character "\n" in the middle. That could be the problem?

Greetings.

D M

unread,
Feb 8, 2021, 9:09:44 AM2/8/21
to ve...@googlegroups.com
Hello Jonad

Unfortunately when I try to provide the buffer in a single line without linefeeds and tags I get this error

PEM contains not enough lines
java.lang.IllegalArgumentException: PEM contains not enough lines
at io.vertx.ext.auth.impl.jose.JWK.parsePEM(JWK.java:266)
at io.vertx.ext.auth.impl.jose.JWK.<init>(JWK.java:249)
at io.vertx.ext.auth.jwt.impl.JWTAuthProviderImpl.<init>(JWTAuthProviderImpl.java:100)
at io.vertx.ext.auth.jwt.JWTAuth.create(JWTAuth.java:42)
Regards

Dominique

--
You received this message because you are subscribed to a topic in the Google Groups "vert.x" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/vertx/CuXhbf0_qGo/unsubscribe.
To unsubscribe from this group and all its topics, send an email to vertx+un...@googlegroups.com.
To view this discussion on the web, visit https://groups.google.com/d/msgid/vertx/4aa0b5fd-5174-450b-8a5e-66272de8f731n%40googlegroups.com.

Jonad García San Martín

unread,
Feb 8, 2021, 9:20:51 AM2/8/21
to vert.x
I faced that issue the last week and fix it adding only the tags and linefeeds to separate from the key. But inside the key I don't need to add a any linefeed. That error, in my case was because in the class io.vertx.ext.auth.impl.jose.JWK there is a method parsePEM that requires at least 3 lines to parse the PEM. and verifies the tags with:

Pattern begin = Pattern.compile("-----BEGIN (.+?)-----");
    Pattern end = Pattern.compile("-----END (.+?)-----");

So, I think you don't need a linefeed at the end of PEM, neither in the middel of the key.
 

Dominique Morel

unread,
Feb 8, 2021, 10:03:13 AM2/8/21
to vert.x
Thanks Jonad for this suggestion. Did you face this with vert.x 4.0.0 or 4.0.2 ? Because my issue happens only in 4.0.2.

However my main issue (whatever 1 on 2 lines for the public key) is the "java.lang.IllegalArgumentException: Illegal base64 character 20" happening because of the space char inside the BEGIN, and don't undestand why it considers this part as the base 64 encoded public key

Dominique

Jonad García San Martín

unread,
Feb 8, 2021, 10:40:58 AM2/8/21
to vert.x

Dominique I look into the stack trace and found that your code always will throws that exception with "buffer" option in your  pubSecKeys. When vertx find that key in your config it decodes the key  with Decoder.RFC4648 or Decoder.RFC4648_URLSAFE, both constructs Decoder with isMIME parameter = false. And in java.util.Base64.decode(byte[] src, int sp, int sl, byte[] dst) when isMIME==false throws that exception.

 

The problem is that in io.vertx.core.json.impl.JsonUtil static block code never uses Base64.getMIMEDecoder() that is the only one that not throws that exception because uses Decoder.RFC2045. I don’t know if that is a bug in vertx 4.0.2 or if it is another thing.

I think you can try to remove the buffer key of the json and only let the public key. Maybe that forces vertx to use the deprecated io.vertx.ext.auth.setPublicKey(String publicKey). With that change, if throws “java.lang.IllegalArgumentException: PEM contains not enough lines” then set

 

“publicKey”:”-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMy9p7xiTEhRvaYHuA9i1T/f2Z2A6qyCbR3abUdR5G/A6TFcfoow0InVfBTcpyMFW3DBaclWgqC3piMQXEreQ8Q==\n-----END PUBLIC KEY-----”

 

I hope that fix your problem. My current problem is with an io.vertx.core.impl.NoStackTraceThrowable exception. I can’t figure out where is the real problem and anyone in the group answer my question =( 

Dominique Morel

unread,
Feb 8, 2021, 11:15:35 AM2/8/21
to vert.x
Actually with vert.x 3.9.2 I specified only the "publicKey" in my configuration.
The "buffer" was required by the 4.0.0 code as you can see in the io.vertx.ext.auth.impl.jose.JWK class : 

public JWK(PubSecKeyOptions options) {

alg = options.getAlgorithm();
kid = options.getId();

final String pem = Objects.requireNonNull(options.getBuffer());


So, leaving the "buffer" not valued always produces a NPE... 

java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:221)
at io.vertx.ext.auth.impl.jose.JWK.<init>(JWK.java:173)
at io.vertx.ext.auth.jwt.impl.JWTAuthProviderImpl.<init>(JWTAuthProviderImpl.java:100)
at io.vertx.ext.auth.jwt.JWTAuth.create(JWTAuth.java:42)

Thanks for your help in any case, hope someone will help to solve our respective problems. Maybe some information is missing in my conf, I'll try to review it later

Dominique Morel

unread,
Feb 10, 2021, 5:44:03 AM2/10/21
to vert.x
Hi Jonad

Unfortunately I'm still stuck and this is blocking our vert.x 3.X to 4.0 migration.
One question for you if you have some time to investigate on this track, could you just try using vertx 4.0.2 and tell me if you face the same kind of issue ?

Thanks

Dominique

Thomas SEGISMONT

unread,
Feb 10, 2021, 6:54:45 AM2/10/21
to vert.x

You received this message because you are subscribed to the Google Groups "vert.x" group.
To unsubscribe from this group and stop receiving emails from it, send an email to vertx+un...@googlegroups.com.
To view this discussion on the web, visit https://groups.google.com/d/msgid/vertx/4dfd4f34-89a9-47c2-8470-01135a06ff10n%40googlegroups.com.

Jonad García San Martín

unread,
Feb 11, 2021, 12:47:59 AM2/11/21
to vert.x

Hello Dominique,

I am currently migrating to vertx 4.0.2 too and without “buffer” it works for me. But I am using a custom JWTAuth implementation and OAuth2FlowType.PASSWORD flow. I will send the code above and I hope that helps you to adapt it in your case. You will see some comments for you:

//Json config:

"keycloak": {

    "enabled": true,

    "auth-server-url": "https://XXX.com/auth",

    "realm": "XXX",

    "resource": "api-vertx-keycloak",

    "secret": "XXX",

    "username": "XXX",

    "password": "XXX",

    "realm-public-key": "XXX",

    "ssl-required": "external"

  },

"jwt" : {

    "path" : "jwt.jceks",

    "password" : "noysi.secret0.2015"

  },

  "jwtOpenIDC" : {

    "public-key" : "XXX"

  }

 

//JWTAuth creation

JWTAuth jwtAuth = new CustomJWTAuth(vertx, configuration.getJsonObject("jwt"),

        configuration.getJsonObject("jwtOpenIDC", new JsonObject()));

 

//Custom JWTAuth class

public class CustomJWTAuth implements JWTAuth {

  Vertx vertx;

  JWTAuth legacyAuthDelegate;

  AuthenticationProvider openidAuthDelegate;


  public CustomJWTAuth(Vertx vertx, JsonObject legacyJWTConfig, JsonObject openidJWTConfig) { 

    this.vertx = vertx;

    // Build legacy delegate

    JsonObject args = new JsonObject().put("keyStore",

        new JsonObject().put("path", legacyJWTConfig.getString("path")).put("type", "jceks")

            .put("password", legacyJWTConfig.getString("password")));

    JWTAuthOptions authOptions = new JWTAuthOptions(args);

    legacyAuthDelegate = JWTAuth.create(vertx, authOptions);

 

    // Build openid delegate

    String oipPublicKey = openidJWTConfig.getString("public-key", "");

    if (!oipPublicKey.isEmpty()) {

      args = new JsonObject().put("public-key", oipPublicKey).put("permissionsClaimKey",

          "resource_access/uid");

      JWTAuthOptions openidOptions = new JWTAuthOptions(args);

      openidAuthDelegate = JWTAuth.create(vertx, openidOptions);

    }

  }


  @Override

  public String generateToken(JsonObject claims, io.vertx.ext.auth.JWTOptions jwtOptions) {

    return legacyAuthDelegate.generateToken(claims, jwtOptions);

  }

 

  @Override

  public String generateToken(JsonObject claims) {

    return legacyAuthDelegate.generateToken(claims);

  }

 

  @Override

  public void authenticate(JsonObject jsonObject, Handler<AsyncResult<User>> handler) {

    // For compatibility with vertx 4 because they need “token” insted of “jwt”(vertx 3). This was another issue that took me several hours to solve

    jsonObject.put("jwt", jsonObject.getValue("token")); 

    // Run auth through both legacy and openid provider. Auth will fail if none of them succeed.

    legacyAuthDelegate.authenticate(jsonObject).onSuccess(user -> {

      handler.handle(Future.succeededFuture(user));

    }).onFailure(err -> {

      err.printStackTrace();

      if (Objects.isNull(openidAuthDelegate)) {

        handler.handle(Future.failedFuture(err.getCause()));

      } else {

        openidAuthDelegate.authenticate(jsonObject).onSuccess(user -> {

          handler.handle(Future.succeededFuture(user));

        }).onFailure(openidErr -> {

          openidErr.printStackTrace();

          handler.handle(Future.failedFuture(openidErr.getCause()));

        });

      }

    });

  }

}

 

 

//Auth implementation

// I put BEGIN/END PUBLIC KEY here but in the json config will work too

JsonObject keycloakJson =

        new JsonObject().put("realm", configuration.getJsonObject("keycloak").getString("realm"))

            .put("realm-public-key",

                "-----BEGIN PUBLIC KEY-----\n"

                    + configuration.getJsonObject("keycloak").getString("realm-public-key")

                    + "\n-----END PUBLIC KEY-----\n")

            .put("auth-server-url",

                configuration.getJsonObject("keycloak").getString("auth-server-url"))

            .put("ssl-required", configuration.getJsonObject("keycloak").getString("ssl-required"))

            .put("resource", configuration.getJsonObject("keycloak").getString("resource"))

            .put("credentials", new JsonObject().put("secret",

                configuration.getJsonObject("keycloak").getString("secret")));

 

    router.post("/sign-in").produces("application/json").handler(rc -> {

      JsonObject userJson = rc.getBodyAsJson();

      userJson.put("username", userJson.getString("email"));

      userJson.put("password", PasswordEncoder.encode(rc.getBodyAsJson().getString("password")));

      if (configuration.getJsonObject("keycloak").getBoolean("enabled") == true) {

        OAuth2Auth oauth2 = KeycloakAuth.create(vertx, OAuth2FlowType.PASSWORD, keycloakJson);

        oauth2.authenticate(userJson).onSuccess(userResponse -> {

          String httpAuthorizationHeader = userResponse.principal().getString("access_token");

          userJson.put("password", rc.getBodyAsJson().getString("password"));

          users.signIn(userJson, async -> {

            if (async.failed()) {

              rc.response().setStatusCode(404).end(async.cause().getMessage());

            } else {

              User user = async.result();

              String token = security.jwt(new JsonObject().put("uid", user.getId()));

              rc.response().setStatusCode(200).putHeader("X-Subject-Token", token)

                  .end(user.toJson().toString());

            }

          });

        }).onFailure(err -> {

          rc.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();

        });

      } else {

        users.signIn(rc.getBodyAsJson(), async -> {

          if (async.failed()) {

            rc.response().setStatusCode(404).end(async.cause().getMessage());

          } else {

            User user = async.result();

            String token = security.jwt(new JsonObject().put("uid", user.getId()));

            rc.response().setStatusCode(200).putHeader("X-Subject-Token", token)

                .end(user.toJson().toString());

          }

        });

      }

    });

 

    router.post("/sign-up").produces("application/json").handler(rc -> {

      if (configuration.getJsonObject("keycloak").getBoolean("enabled") == true) {

        JsonObject userJson = rc.getBodyAsJson();

        userJson.put("password", PasswordEncoder.encode(rc.getBodyAsJson().getString("password")));

        String userIdK = createAgentOnKeyclock(userJson, configuration.getJsonObject("keycloak"));

        userJson.put("id_keycloak", userIdK);

        if (!userIdK.equals("Failure")) {

          userJson.put("password", rc.getBodyAsJson().getString("password"));

          users.signUp(userJson, async -> {

            if (async.failed()) {

              NoysiException exception = (NoysiException) async.cause();

              rc.response().setStatusCode(exception.getCode()).end();

            } else {

              users.signIn(userJson, async2 -> {

                if (async2.failed()) {

                  rc.response().setStatusCode(404).end(async2.cause().getMessage());

                } else {

                  User user = async2.result();

                  String token = security.jwt(new JsonObject().put("uid", user.getId()));

                  rc.response().setStatusCode(200).putHeader("X-Subject-Token", token)

                      .end(user.toJson().toString());

                }

              });

            }

          });

        } else {

          rc.response().setStatusCode(HttpResponseStatus.CONFLICT.code()).end();

        }

      } else {

        users.signUp(rc.getBodyAsJson(), async -> {

          if (async.failed()) {

            NoysiException exception = (NoysiException) async.cause();

            rc.response().setStatusCode(exception.getCode()).end();

          } else {

            users.signIn(rc.getBodyAsJson(), async2 -> {

              if (async2.failed()) {

                rc.response().setStatusCode(404).end(async2.cause().getMessage());

              } else {

                User user = async2.result();

                String token = security.jwt(new JsonObject().put("uid", user.getId()));

                rc.response().setStatusCode(200).putHeader("X-Subject-Token", token)

                    .end(user.toJson().toString());

              }

            });

          }

        });

      }

….

Alis Rasic

unread,
Jun 18, 2022, 5:25:41 PM6/18/22
to vert.x
Why this is marked as abuse? It has been marked as abuse.
Report not abuse
Joining the party couple of months later, but I guess it's never too late.

I experienced the identical problem and was able to solve it as follows:

1. Use the same format of value as you did, which is:
"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMy9p7xiTEhRvaYHuA9i1T/f2Z2A6\nqyCbR3abUdR5G/A6TFcfoow0InVfBTcpyMFW3DBaclWgqC3piMQXEreQ8Q==\n-----END PUBLIC KEY-----\n"

2. Used a function in java to replace all characters '\n' with '\r\n' as follows:
publicKey.replaceAll("\\\\n","\r\n");
considering that publicKey contains the string value from above, just a recall that double-backslashes are used to escape special characters.

3. Validating by printing/debugging which should output something like 
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMy9p7xiTEhRvaYHuA9i1T/f2Z2A6
qyCbR3abUdR5G/A6TFcfoow0InVfBTcpyMFW3DBaclWgqC3piMQXEreQ8Q==
-----END PUBLIC KEY-----
Reply all
Reply to author
Forward
0 new messages