paseto-toolkit / jpaseto

A library for creating and parsing Paseto in Java
Apache License 2.0
67 stars 15 forks source link

Long Time Taken for Token Creation #9

Closed madanprabhud closed 4 years ago

madanprabhud commented 4 years ago

We are evaluating the wonderful jpaseto library for one of our project. Kindly find our observation and their respective code.

Our primary concern about the 1 minute for token creation and we can't afford this at our production (Time mentioned in Milliseconds).

Kindly check the primary testing code and give your suggestion if any improvements.

Our Testing System Configuration : system HP ProBook 440 G6 processor Intel(R) Core(TM) i7-8565U CPU @ 1.80GH memory 8GiB SODIMM DDR4 Synchronous 3200 MHz memory 256KiB L1 cache memory 1MiB L2 cache

`public class JPasetoExample { private static final SecretKey SHARED_SECRET = Keys.secretKey();

public static void main(String[] args) {

    System.out.print("Secret Key in Base64 -- " + Base64.getEncoder().encodeToString(SHARED_SECRET.getEncoded())+"\n");

    long start = System.currentTimeMillis();

    String tokenString = createToken();

    long end = System.currentTimeMillis();

    log(" Token Creation Time -- " + (end-start));
    log("Paseto token: "+ tokenString);

    start = System.currentTimeMillis();
    Paseto result = parseToken(tokenString);
    end = System.currentTimeMillis();
    log(" Token Parse Time -- " + (end-start));

    start = System.currentTimeMillis();
    log("Token Claims:");
    result.getClaims().forEach((key, value) -> log("    "+ key + ": " + value));
    end = System.currentTimeMillis();
    log(" Token Claims Traversal -- " + (end-start));

    start = System.currentTimeMillis();
    String audience = result.getClaims().getAudience();
    log("Audience: "+ audience);
    end = System.currentTimeMillis();
    log(" Token get Audience -- " + (end-start));

    start = System.currentTimeMillis();
    int rolledValue = result.getClaims().get("1d20", Integer.class);
    log("1d20 rolled: " + rolledValue);
    end = System.currentTimeMillis();
    log(" Token Custome Claim -- " + (end-start));

    start = System.currentTimeMillis();
    parseTokenWithRequirements(tokenString);
    end = System.currentTimeMillis();
    log(" Token Parse Time with Requirements -- " + (end-start));
}

public static String createToken() {
    Instant now = Instant.now();

    String token = Pasetos.V1.LOCAL.builder()
            .setSharedSecret(SHARED_SECRET)
            .setIssuedAt(now)
            .setExpiration(now.plus(1, ChronoUnit.HOURS))
            .setAudience("blog-post")
            .setIssuer("https://developer.okta.com/blog/")
            .claim("1d20", new Random().nextInt(20) + 1)
            .compact();

    return token;
}

public static Paseto parseToken(String token) {
    PasetoParser parser = Pasetos.parserBuilder()
            .setSharedSecret(SHARED_SECRET)
            .build();

    Paseto result = parser.parse(token);
    return result;
}

public static Paseto parseTokenWithRequirements(String token) {
    PasetoParser parser = Pasetos.parserBuilder()
            .setSharedSecret(SHARED_SECRET)
            .requireAudience("blog-post")
            .requireIssuer("https://developer.okta.com/blog/")
            .build();

    Paseto result = parser.parse(token);
    return result;
}

private static void log(String message) {
    System.out.println(message);
}

}`

Output :

Secret Key in Base64 -- JdhPEhZ2B87HmbJ+6JOoA+uAMFnZEKqQbMOSWXdUFQo= Token Creation Time -- 91076 Paseto token: v1.local.Dgw158XSwr7sxzFbtkXfoOIFu37_gxEUNX5D6KLwhMdCqlwQCBJaFJyMDPPpSXEhHMWxLDj49vCLzvqS-e0K4bwJpmz90WrRNpESkjP1DFoisJUu5KQpHgZ8xjsgrxLgfyQoPIBLcZ61nTpDFmb6UdLQQQmqy21_vMELM_sSpv-1Su-IUtKyvEfgwkAevIW7-vAO_AdHJ1TnFFySRzqhmRjKbpKs1qXqFBLP4l2rM9hSUDcJeZBaXqwi1OxshKlND2LEbk2SL79paaSfC-doDblbbhmGrbs3z9HoyKdKawJnwLQuXKi1 Token Parse Time -- 21 Token Claims: aud: blog-post 1d20: 7 iss: https://developer.okta.com/blog/ exp: 2020-08-14T08:39:18.594777+00:00 iat: 2020-08-14T07:39:18.594777+00:00 Token Claims Traversal -- 1 Audience: blog-post Token get Audience -- 0 1d20 rolled: 7 Token Custome Claim -- 0 Token Parse Time with Requirements -- 2

zbiljic commented 4 years ago

Hello, I just saw this, and figured I might offer valid comment about this.

My first thought was that this was due to initial loading of some libraries required to actually produce the token, and I was right. Your observed increased time was due to initial loading of various required libraries in JVM. The crypto libraries cary some weight, and require some "loading" time. (Some other parts may have small problem also.)

Please note that this is just for the first time in the running JVM.

If you are to load required parts in the static block, or before actually creating token you would have millisecond time for creation.

The easiest way to validate this is to wrap the content from the main method block in a for loop, with few (i.e. 10) iterations. Even in the second you should save same or smaller time for token creation. If you are to increase the iteration to 100 or 1000 you might get sub-millisecond time for token creation and parse time.

P.S. Another point, that may not be needed for your case, is that there is better way to do benchmarking in Java, using excellent JMH project.

madanprabhud commented 4 years ago

Hello, I just saw this, and figured I might offer valid comment about this.

My first thought was that this was due to initial loading of some libraries required to actually produce the token, and I was right. Your observed increased time was due to initial loading of various required libraries in JVM. The crypto libraries cary some weight, and require some "loading" time. (Some other parts may have small problem also.)

Please note that this is just for the first time in the running JVM.

If you are to load required parts in the static block, or before actually creating token you would have millisecond time for creation.

The easiest way to validate this is to wrap the content from the main method block in a for loop, with few (i.e. 10) iterations. Even in the second you should save same or smaller time for token creation. If you are to increase the iteration to 100 or 1000 you might get sub-millisecond time for token creation and parse time.

P.S. Another point, that may not be needed for your case, is that there is better way to do benchmarking in Java, using excellent JMH project.

Hi zbilijic,

Thanks for your quick resolution.

Iterated for multiple times also, duration for token creation seems to be quite higher. Kindly just the best way of creating token for encrypted v1.

 Token Creation Time -- 11871
 Token Creation Time -- 76723
 Token Creation Time -- 35702
 Token Creation Time -- 62972
 Token Creation Time -- 17359

Even, we are planning to use as REST service for login authentication with PASETO token as response. So, we can't iterate for the single logon use.

So, Kindly share some code-snippets for the faster token creation.

madanprabhud commented 4 years ago

My Testing OS is Linux.

Is it due to the Secure Random functionality?

If so, how can handle more effectively?

zbiljic commented 4 years ago

I did not want to expand my comment too much, although several things are left open.

To begin with (as I mentioned), this is some code based on your example:

import dev.paseto.jpaseto.Pasetos;
import dev.paseto.jpaseto.lang.Keys;

import javax.crypto.SecretKey;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class JPasetoExample {

  private static final SecretKey SHARED_SECRET = Keys.secretKey();

  public static void main(String[] args) {
    System.out.print("Secret Key in Base64 -- " + Base64.getEncoder().encodeToString(SHARED_SECRET.getEncoded()) + "\n");

    // generate multiple tokens
    for (int i = 0; i < 100; i++) {
      long start = System.nanoTime();
      String tokenString = createToken();
      long end = System.nanoTime();
      System.out.printf("[%d] Token [%s] Creation Time -- %d\n", i, tokenString.substring(0, 10), TimeUnit.MILLISECONDS.convert(end - start, TimeUnit.NANOSECONDS));
    }
  }

  public static String createToken() {
    Instant now = Instant.now();

    String token = Pasetos.V1.LOCAL.builder()
        .setSharedSecret(SHARED_SECRET)
        .setIssuedAt(now)
        .setExpiration(now.plus(1, ChronoUnit.HOURS))
        .setAudience("blog-post")
        .setIssuer("https://developer.okta.com/blog/")
        .claim("1d20", new Random().nextInt(20) + 1)
        .compact();

    return token;
  }
}

Running this example code snippet, I have following results:

Secret Key in Base64 -- GlZnbYM9N6xkcUPGaN4loeoDSckEdGpGB2o9LxoXIFE=
[0] Token [v1.local.d] Creation Time -- 1034
[1] Token [v1.local.p] Creation Time -- 4
[2] Token [v1.local.V] Creation Time -- 5
[3] Token [v1.local.N] Creation Time -- 2
...
[96] Token [v1.local.T] Creation Time -- 0
[97] Token [v1.local.X] Creation Time -- 0
[98] Token [v1.local.o] Creation Time -- 1
[99] Token [v1.local.j] Creation Time -- 1

This class was just added in "intergration-tests" project of paseto-toolkit/jpaseto. In order to know exactly why you have the issue, you should share whole example project that you use, in order for someone to be able to try to reproduce locally in entirety.

By only providing example that you did, I could only try that one, and can say that it works. The first time token is created will be slow, however each subsequent should take around single millisecond.

madanprabhud commented 4 years ago

It's due to the secure random. Installed haveged and now i got the tokens created at only 700 milliseconds.

Many Linux distros (mostly Debian-based) configure OpenJDK to use /dev/random for entropy.

/dev/random is by definition slow (and can even block).

From here you have two options on how to unblock it:

Improve entropy, or Reduce randomness requirements. Option 1, Improve entropy

To get more entropy into /dev/random, try the haveged daemon. It's a daemon that continuously collects HAVEGE entropy, and works also in a virtualized environment because it doesn't require any special hardware, only the CPU itself and a clock.

On Ubuntu/Debian:

apt-get install haveged update-rc.d haveged defaults service haveged start On RHEL/CentOS:

yum install haveged systemctl enable haveged systemctl start haveged Option 2. Reduce randomness requirements

If for some reason the solution above doesn't help or you don't care about cryptographically strong randomness, you can switch to /dev/urandom instead, which is guaranteed not to block.

To do it globally, edit the file jre/lib/security/java.security in your default Java installation to use /dev/urandom (due to another bug it needs to be specified as /dev/./urandom).

Like this:

securerandom.source=file:/dev/random

securerandom.source=file:/dev/./urandom Then you won't ever have to specify it on the command line.

bdemers commented 4 years ago

@madanprabhud Thanks for following up!

Maybe it's worth adding a note about this in our docs? What distro are you using, and which OpenJDK (vendor and version)?

madanprabhud commented 4 years ago

@bdemers

I though to convey the same. Kindly keep this tweak for better performance as note in the documentation.

Please find my Distro and JVM version as follows.,

Distributor ID: LinuxMint Description: Linux Mint 19.3 Tricia Release: 19.3 Codename: tricia

openjdk version "11.0.8" 2020-07-14 OpenJDK Runtime Environment (build 11.0.8+10-post-Ubuntu-0ubuntu118.04.1) OpenJDK 64-Bit Server VM (build 11.0.8+10-post-Ubuntu-0ubuntu118.04.1, mixed mode, sharing)

One more info: in jpasteo example at https://github.com/oktadeveloper/okta-jpaseto-example/blob/master/src/main/java/com/okta/example/JPasetoExample.java

What's the need to specifying the Asymmetric key with symmetric key in only at parsing without configure anything at token creation time?

Kindly explain to me if anything wrong in my understanding.

bdemers commented 4 years ago

I’ll try to improve the example. But the TL;DR is a parser instance can parser all types of token (if/when the keys are configured) but a token builder can only create a single type of token.

In normal usage you likely will only be dealing with a single token type

ghost commented 2 years ago

jwt is faster and more secure than papeto i swtiched recently works good I also pay $2 per month for devs who work at jwt to improve perfomance of database queriing