Skip to content

Latest commit

 

History

History
383 lines (275 loc) · 13.4 KB

README.adoc

File metadata and controls

383 lines (275 loc) · 13.4 KB

Securing and controlling access

Tip
The corresponding source code is in the step-7 folder of the guide repository.

Securing and controlling access is easy to do with Vert.x. In this section, we will:

  1. move from HTTP to HTTPS, and

  2. add user authentication with group-based privileges to the web application, and

  3. control access to the web API using JSON web tokens (JWT).

HTTPS support in Vert.x

Vert.x provides support for SSL-encrypted network connections. It is common to expose HTTP servers in production through a front HTTP server / proxy like Nginx, and have it use HTTPS for incoming connections. Vert.x can also expose HTTPS by itself, so as to provide end-to-end encryption.

Certificates can be stored in Java KeyStore files. You will likely need a self-signed certificate for testing purposes, and here is how to create one in a server-keystore.jks KeyStore where the password is secret:

link:gen-keystore.sh[role=include]

We can then change the HTTP server creation to pass a HttpServerOptions object to specify that we want SSL, and to point to our KeyStore file:

link:src/main/java/io/vertx/guides/wiki/http/HttpServerVerticle.java[role=include]

We can point a web browser to https://localhost:8080/, but since the certificate is a self-signed one any good browser will rightfully yield a security warning:

invalid cert

Last but not least, we need to update the test case in ApiTest since the original code was made for issuing HTTP requests with the web client:

link:src/test/java/io/vertx/guides/wiki/http/ApiTest.java[role=include]
  1. Ensures SSL.

  2. Since the certificate is self-signed, we need to explicitly trust it otherwise the web client connections will fail just like a web browser would.

Access control and authentication

Vert.x provides a wide range of options for doing authentication and authorization. The officially supported modules cover JDBC, MongoDB, Apache Shiro, htdigest files, OAuth2 with well-known providers and JWT (JSON web tokens).

While the next section will cover JWT, this one focuses on using JDBC-based authentication, reusing the wiki database.

The goal is to require users to authenticate for using the wiki, and have role-based permissions:

  • having no role only allows reading pages,

  • having a writer role allows editing pages,

  • having an editor role allows creating, editing and deleting pages,

  • having an admin role is equivalent to having all possible roles.

Refactoring the JDBC configuration

In the previous steps, only the database verticle and service where aware of the JDBC configuration. Now the HTTP service vertice also needs to share the same JDBC access.

To do that we extract configuration parameters and default values to an interface:

link:src/main/java/io/vertx/guides/wiki/DatabaseConstants.java[role=include]

Now in WikiDatabaseVerticle we use these constants as follows:

link:src/main/java/io/vertx/guides/wiki/database/WikiDatabaseVerticle.java[role=include]

Adding JDBC authentication to routes

The first step is to add the vertx-auth-jdbc module to the Maven dependencies list:

link:pom.xml[role=include]

Back to the HttpServerVerticle class code, we create an authentication provider:

link:src/main/java/io/vertx/guides/wiki/http/HttpServerVerticle.java[role=include]

The JDBCAuth object instance is then used to deal with server-side user sessions:

link:src/main/java/io/vertx/guides/wiki/http/HttpServerVerticle.java[role=include]
  1. We install a user session handler for all routes.

  2. This automatically redirects requests to /login when there is no user session for the request.

  3. We install authHandler for all routes where authentication is required.

Finally, we need to create 3 routes for displaying a login form, handling login form submissions and logging out users:

link:src/main/java/io/vertx/guides/wiki/http/HttpServerVerticle.java[role=include]
  1. FormLoginHandler is a helper for processing login submission requests. By default it expects the HTTP POST request to have: username as the login, password as the password, and return_url as the URL to redirect to upon success.

  2. Logging out a user is a simple as clearing it from the current RoutingContext.

The code for the loginHandler method is:

link:src/main/java/io/vertx/guides/wiki/http/HttpServerVerticle.java[role=include]

The HTML template is placed in src/main/resources/templates/login.ftl:

link:src/main/resources/templates/login.ftl[role=include]

The login page looks as follows:

login form

Supporting features based on roles

Features need to be activated only if the user has sufficient permissions. In the following screenshot an administrator can create a page and perform a backup:

as root

By contrast a user with no role cannot perform these actions:

as baz

To do that, we can access the RoutingContext user reference, and query for permissions. Here is how this is implemented for the indexHandler handler method:

link:src/main/java/io/vertx/guides/wiki/http/HttpServerVerticle.java[role=include]
  1. This is how a permission query is made. Note that this is an asynchronous operation.

  2. We use the result to…​

  3. …​leverage it in the HTML template.

  4. We also have access to the user login.

The template code has been modified to only render certain fragments based on the value of canCreatePage:

link:src/main/resources/templates/index.ftl[role=include]

The code is similar for ensuring that updating or deleting a page is restricted to certain roles and is available from the guide Git repository.

It is important to ensure that checks are also being done on HTTP POST request handlers and not just when rendering HTML pages. Indeed, malicious attackers could still craft requests and perform actions while not being authenticated. Here is how to protect page deletions by wrapping the pageDeletionHandler code inside a topmost permission check:

link:src/main/java/io/vertx/guides/wiki/http/HttpServerVerticle.java[role=include]

Populating the database with user and role data

A last step is required for assembling all pieces of our authentication puzzle. We leave adding user registration and management as an exercice left to the reader, and instead we add some code to ensure that the database is being populated with some roles and accounts:

Login Password Roles

root

admin

admin (create, delete, update)

foo

bar

editor (create, delete, update), writer (update)

bar

baz

writer (update)

baz

baz

/

The vertx-auth-jdbc prescribes a default database schema with 3 tables: 1 for users (with salted passwords), 1 for role permissions, and 1 to map users to roles. That schema can be changed and configured, but in many cases sticking to the default is a very good option.

To do that, we are going to deploy a verticle whose sole role is performing the initialisation work:

link:src/main/java/io/vertx/guides/wiki/http/AuthInitializerVerticle.java[role=include]
  1. The hashed values can be generated using auth.computeHash(password, salt) where auth is a JDBCAuth instance. Salt values can also be generated using auth.generateSalt().

This impacts the MainVerticle class, as we now deploy it first:

link:src/main/java/io/vertx/guides/wiki/MainVerticle.java[role=include]

Finally, we also need to update the ApiTest class to setup the in-memory database for both the authentication and wiki storage:

link:src/test/java/io/vertx/guides/wiki/http/ApiTest.java[role=include]

Authenticating web API requests with JWT

JSON Web Tokens (RFC 7519) is a standard for issuing JSON-encoded tokens containing claims, typically identifying a user and permissions, although claims can be just about anything.

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.

Warning
While a JWT token is signed, its content is not encrypted. It must be transported over a secure channel (e.g., HTTPS) and it should never have sensitive data as a claim (e.g., passwords, private API keys, etc).

Adding JWT support

We start by adding the vertx-auth-jwt module to the Maven dependencies:

link:pom.xml[role=include]

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

link:src/main/java/io/vertx/guides/wiki/http/HttpServerVerticle.java[role=include]

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

link:src/main/java/io/vertx/guides/wiki/http/HttpServerVerticle.java[role=include]
  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. Upon success we can query for roles.

  3. 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:

link:src/main/java/io/vertx/guides/wiki/http/HttpServerVerticle.java[role=include]

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: */*
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:

$ http --verbose --verify no GET https://localhost:8080/api/pages
GET /api/pages HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.8



HTTP/1.1 401 Unauthorized
Content-Length: 12

Unauthorized

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

The ApiTest class needs to be updated to support JWT tokens.

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

link:src/test/java/io/vertx/guides/wiki/http/ApiTest.java[role=include]

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

link:src/test/java/io/vertx/guides/wiki/http/ApiTest.java[role=include]
  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:

link:src/test/java/io/vertx/guides/wiki/http/ApiTest.java[role=include]
  1. We store the token with the Bearer prefix to the field for the next requests.

  2. We pass the token as a header.