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:
keytool -genseckey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg HMacSHA256 -keysize 2048 -alias HS256 -keypass secret
keytool -genseckey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg HMacSHA384 -keysize 2048 -alias HS384 -keypass secret
keytool -genseckey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg HMacSHA512 -keysize 2048 -alias HS512 -keypass secret
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
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
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
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
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
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:
Router apiRouter = Router.router(vertx);
JWTAuth jwtAuth = JWTAuth.create(vertx, new JWTAuthOptions()
.setKeyStore(new KeyStoreOptions()
.setPath("keystore.jceks")
.setType("jceks")
.setPassword("secret")));
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:
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.
We generate a token with
username
,canCreate
,canDelete
andcanUpdate
claims.
Each API handler method can now query the current user principal and claims. Here is how the apiDeletePage
does it:
private void apiDeletePage(RoutingContext context) {
if (context.user().principal().getBoolean("canDelete", false)) {
int id = Integer.valueOf(context.request().getParam("id"));
dbService.deletePage(id, reply -> {
handleSimpleDbReply(context, reply);
});
} else {
context.fail(401);
}
}
Using JWT tokens
To illustrate how to work with JWT tokens, let’s create a new one for the root
user:
- $ http --verbose --verify no GET https://localhost:8080/api/token login:root password:w00t
- GET /api/token HTTP/1.1
- Accept-Encoding: gzip, deflate
- Connection: keep-alive
- Host: localhost:8080
- User-Agent: HTTPie/0.9.8
- login: root
- password: w00t
- HTTP/1.1 200 OK
- Content-Length: 242
- Content-Type: text/plain
- Set-Cookie: vertx-web.session=8cbb38ac4ce96737bfe31cc0ceaae2b9; Path=/
- 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:
- $ http --verbose --verify no GET https://localhost:8080/api/pages Authorization:'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJvb3QiLCJjYW5DcmVhdGUiOnRydWUsImNhbkRlbGV0ZSI6dHJ1ZSwiY2FuVXBkYXRlIjp0cnVlLCJpYXQiOjE0ODk0NDE1OTAsImlzcyI6IlZlcnQueCIsInN1YiI6Ildpa2kgQVBJIn0=.RmtJb81QKVUFreXL-ajZ8ktLGasoKEqG8GSQncRWrN8='
- GET /api/pages HTTP/1.1
- Accept: */*
- Accept-Encoding: gzip, deflate
- Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJvb3QiLCJjYW5DcmVhdGUiOnRydWUsImNhbkRlbGV0ZSI6dHJ1ZSwiY2FuVXBkYXRlIjp0cnVlLCJpYXQiOjE0ODk0NDE1OTAsImlzcyI6IlZlcnQueCIsInN1YiI6Ildpa2kgQVBJIn0=.RmtJb81QKVUFreXL-ajZ8ktLGasoKEqG8GSQncRWrN8=
- Connection: keep-alive
- Host: localhost:8080
- User-Agent: HTTPie/0.9.8
- HTTP/1.1 200 OK
- Content-Length: 99
- Content-Type: application/json
- Set-Cookie: vertx-web.session=0598697483371c7f3cb434fbe35f15e4; Path=/
- {
- "pages": [
- {
- "id": 0,
- "name": "Hello"
- {
- "id": 1,
- "name": "Apple"
- },
- {
- "id": 2,
- "name": "Vert.x"
- }
- ],
- "success": true
- }
Adapting the API test fixture
We add a new field for retrieving the token value to be used in test cases:
private String jwtTokenHeaderValue;
We add first step to retrieve a JTW token authenticated as user :
Credentials are passed as headers.
The response payload is of
text/plain
type, so we use that for the body decoding codec.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:
Future<HttpResponse<JsonObject>> postPageFuture = tokenFuture.compose(tokenResponse -> {
Promise<HttpResponse<JsonObject>> promise = Promise.promise();
jwtTokenHeaderValue = "Bearer " + tokenResponse.body(); (1)
webClient.post("/api/pages")
.putHeader("Authorization", jwtTokenHeaderValue) (2)
.as(BodyCodec.jsonObject())
.sendJsonObject(page, promise);
return promise.future();
});
Future<HttpResponse<JsonObject>> getPageFuture = postPageFuture.compose(resp -> {
Promise<HttpResponse<JsonObject>> promise = Promise.promise();
webClient.get("/api/pages")
.putHeader("Authorization", jwtTokenHeaderValue)
.as(BodyCodec.jsonObject())
.send(promise);
return promise.future();
});
We pass the token as a header.