A token is issued by a server and it is signed with the server key. A client can send a token back along with subsequent requests: both the client and the server can check that a token is authentic and unaltered.

    Adding JWT support

    We start by adding the module to the Maven dependencies:

    We will have a JCEKS keystore to hold the keys for our tests. Here is how to generate a keystore.jceks with the suitable keys of various lengths:

    1. keytool -genseckey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg HMacSHA256 -keysize 2048 -alias HS256 -keypass secret
    2. keytool -genseckey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg HMacSHA384 -keysize 2048 -alias HS384 -keypass secret
    3. keytool -genseckey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg HMacSHA512 -keysize 2048 -alias HS512 -keypass secret
    4. keytool -genkey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg RSA -keysize 2048 -alias RS256 -keypass secret -sigalg SHA256withRSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
    5. keytool -genkey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg RSA -keysize 2048 -alias RS384 -keypass secret -sigalg SHA384withRSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
    6. keytool -genkey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg RSA -keysize 2048 -alias RS512 -keypass secret -sigalg SHA512withRSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
    7. keytool -genkeypair -keystore keystore.jceks -storetype jceks -storepass secret -keyalg EC -keysize 256 -alias ES256 -keypass secret -sigalg SHA256withECDSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
    8. keytool -genkeypair -keystore keystore.jceks -storetype jceks -storepass secret -keyalg EC -keysize 256 -alias ES384 -keypass secret -sigalg SHA384withECDSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
    9. keytool -genkeypair -keystore keystore.jceks -storetype jceks -storepass secret -keyalg EC -keysize 256 -alias ES512 -keypass secret -sigalg SHA512withECDSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360

    We need to install a JWT token handler on API routes:

    1. Router apiRouter = Router.router(vertx);
    2. JWTAuth jwtAuth = JWTAuth.create(vertx, new JWTAuthOptions()
    3. .setKeyStore(new KeyStoreOptions()
    4. .setPath("keystore.jceks")
    5. .setType("jceks")
    6. .setPassword("secret")));
    7. apiRouter.route().handler(JWTAuthHandler.create(jwtAuth, "/api/token"));

    We pass /api/token as a parameter for the object creation to specify that this URL shall be ignored. Indeed, this URL is being used to generate new JWT tokens:

    1. We expect login and password information to have been passed through HTTP request headers, and we authenticate using the authentication provider of the previous section.

    2. We generate a token with username, canCreate, canDelete and canUpdate claims.

    Each API handler method can now query the current user principal and claims. Here is how the apiDeletePage does it:

    1. private void apiDeletePage(RoutingContext context) {
    2. if (context.user().principal().getBoolean("canDelete", false)) {
    3. int id = Integer.valueOf(context.request().getParam("id"));
    4. dbService.deletePage(id, reply -> {
    5. handleSimpleDbReply(context, reply);
    6. });
    7. } else {
    8. context.fail(401);
    9. }
    10. }

    Using JWT tokens

    To illustrate how to work with JWT tokens, let’s create a new one for the root user:

    1. $ http --verbose --verify no GET https://localhost:8080/api/token login:root password:w00t
    2. GET /api/token HTTP/1.1
    3. Accept-Encoding: gzip, deflate
    4. Connection: keep-alive
    5. Host: localhost:8080
    6. User-Agent: HTTPie/0.9.8
    7. login: root
    8. password: w00t
    9.  
    10.  
    11.  
    12. HTTP/1.1 200 OK
    13. Content-Length: 242
    14. Content-Type: text/plain
    15. Set-Cookie: vertx-web.session=8cbb38ac4ce96737bfe31cc0ceaae2b9; Path=/
    16.  
    17. eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJvb3QiLCJjYW5DcmVhdGUiOnRydWUsImNhbkRlbGV0ZSI6dHJ1ZSwiY2FuVXBkYXRlIjp0cnVlLCJpYXQiOjE0ODk0NDE1OTAsImlzcyI6IlZlcnQueCIsInN1YiI6Ildpa2kgQVBJIn0=.RmtJb81QKVUFreXL-ajZ8ktLGasoKEqG8GSQncRWrN8=

    The response text is the token value and shall be retained.

    We can check that performing an API request without the token results in a denial of access:

    Sending a JWT token along with a request is done using a Authorization HTTP request header where the value must be Bearer <token value>. Here is how to fix the API request above by passing the JWT token that had been issued to us:

    1. $ http --verbose --verify no GET https://localhost:8080/api/pages Authorization:'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJvb3QiLCJjYW5DcmVhdGUiOnRydWUsImNhbkRlbGV0ZSI6dHJ1ZSwiY2FuVXBkYXRlIjp0cnVlLCJpYXQiOjE0ODk0NDE1OTAsImlzcyI6IlZlcnQueCIsInN1YiI6Ildpa2kgQVBJIn0=.RmtJb81QKVUFreXL-ajZ8ktLGasoKEqG8GSQncRWrN8='
    2. GET /api/pages HTTP/1.1
    3. Accept: */*
    4. Accept-Encoding: gzip, deflate
    5. Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJvb3QiLCJjYW5DcmVhdGUiOnRydWUsImNhbkRlbGV0ZSI6dHJ1ZSwiY2FuVXBkYXRlIjp0cnVlLCJpYXQiOjE0ODk0NDE1OTAsImlzcyI6IlZlcnQueCIsInN1YiI6Ildpa2kgQVBJIn0=.RmtJb81QKVUFreXL-ajZ8ktLGasoKEqG8GSQncRWrN8=
    6. Connection: keep-alive
    7. Host: localhost:8080
    8. User-Agent: HTTPie/0.9.8
    9.  
    10.  
    11.  
    12. HTTP/1.1 200 OK
    13. Content-Length: 99
    14. Content-Type: application/json
    15. Set-Cookie: vertx-web.session=0598697483371c7f3cb434fbe35f15e4; Path=/
    16.  
    17. {
    18. "pages": [
    19. {
    20. "id": 0,
    21. "name": "Hello"
    22. {
    23. "id": 1,
    24. "name": "Apple"
    25. },
    26. {
    27. "id": 2,
    28. "name": "Vert.x"
    29. }
    30. ],
    31. "success": true
    32. }

    Adapting the API test fixture

    We add a new field for retrieving the token value to be used in test cases:

    1. private String jwtTokenHeaderValue;

    We add first step to retrieve a JTW token authenticated as user :

    1. Credentials are passed as headers.

    2. The response payload is of text/plain type, so we use that for the body decoding codec.

    3. Upon success we complete the tokenRequest future with the token value.

    Using the JWT token is now a matter of passing it back as a header to HTTP requests:

    1. Future<HttpResponse<JsonObject>> postPageFuture = tokenFuture.compose(tokenResponse -> {
    2. Promise<HttpResponse<JsonObject>> promise = Promise.promise();
    3. jwtTokenHeaderValue = "Bearer " + tokenResponse.body(); (1)
    4. webClient.post("/api/pages")
    5. .putHeader("Authorization", jwtTokenHeaderValue) (2)
    6. .as(BodyCodec.jsonObject())
    7. .sendJsonObject(page, promise);
    8. return promise.future();
    9. });
    10. Future<HttpResponse<JsonObject>> getPageFuture = postPageFuture.compose(resp -> {
    11. Promise<HttpResponse<JsonObject>> promise = Promise.promise();
    12. webClient.get("/api/pages")
    13. .putHeader("Authorization", jwtTokenHeaderValue)
    14. .as(BodyCodec.jsonObject())
    15. .send(promise);
    16. return promise.future();
    17. });
    1. We pass the token as a header.