Getting Started with Dropwizard: Authentication, Configuration and HTTPS
Basic Authentication is the simplest way to secure access to a resource.
Join the DZone community and get the full member experience.
Join For FreeIn the previous installment of this series we discussed how to create a Dropwizard project using Maven archetype as well as how to create a simple RESTful API and access it. API security is an important topic and today we'll discuss how to use authentication and HTTPS with Dropwizard. Also, the problem of configuring Dropwizard applications will be touched. All the code for the examples below can be found here .
Basic Authentication is a simplest way to secure access to a resource. It boils down to transmitting a base64-encoded column-separated pair of user-ID and password using Authorize HTTP header. If a non-authenticated client tries to access a protected resource, the server prompts the client to provide credentials, that is the aforementioned pair. Otherwise, client may supply the credentials without any prompt from the server.
This authentication scheme is a non-secure one as it uses unencrypted credentials. The encoding is used in order to replace non-HTTP compatible characters with compatible ones and can easily be reversed by a snooper. It is good practice to use this scheme in conjunction with HTTPS whereby credentials are transmitted over an encrypted channel.
To start working with authentication on should add a dependency to the pom-file.
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-auth</artifactId>
<version>${dropwizard.version}</version>
</dependency>
Let's talk about how to add Basic Authentication to your Dropwizard REST API and then discuss how to access secured resources both from command line and browser and finally, at the end of this article, we'll see how to test password-protected sub-resource methods. To secure a resource in Dropwizard one uses a three-step approach. First, one should implement an Authenticator interface where a method verifying user credentials is defined. It is up to the API developer how to store and verify credentials; it is possible to store them in a database or a LDAP server, both options are supported by Dropwizard. The method can return some object with user data which can be displayed to the user after authentication succeeds. Let's see a snippet.
public class GreetingAuthenticator
implements Authenticator<BasicCredentials, User> {
@Override
public Optional<User> authenticate(BasicCredentials credentials)
throws AuthenticationException {
if ("crimson".equals(credentials.getPassword())) {
return Optional.of(new User());
} else {
return Optional.absent();
}
}
}
One should create a com.javaeeeee.dwstart.auth package to place the code. First of all let's dissect the authenticate method. It uses a hard-coded password and no user-ID at all. While this makes code simple to comprehend, here should be the code which hits the database or the LDAP server, which checks the credentials. The return of this method has something to do with a User class which is defined in a com.javaeeeee.dwstart.core package. It is up to you as a developer what fields to add to this class. It could be user full name, e-mail, etc. Even an empty class would do the trick.
If you look at the definition of our GreetingAuthenticator class you'll see that instead of the User type argument one may use some other class and the type is specified in the angle brackets as the second type parameter. The first type argument is BasicCredentials which supplies the authenticate method with user credentials.
As it was mentioned before, the authenticate method is a place to do some kind of lookup and the result could be that the user is present or that the user is absent in the storage of our API. The conventional way to treat the user-is-absent situation is to use a null as a returned result. However, there is a better way to do the job, namely to have a special class as a return type, objects of which can either store a reference to another object or be empty. Sometimes it is not clear whether a method can return null, so it is better to show explicitly that the situation when the result is absent is possible using Optional return type. These leads to less possible errors when processing the result of such method, as one is informed by the method's return type that the result can be absent and prompted to treat such path. The class is a part of Google Guava libraries set, which is a member of Dropwizard family.
To create an Optional object one can use either the of(...) method by passing a non-null reference to it, or the absent() method to create an empty object. There is the third option to use the fromNullable(...) method whereby a possibly-null reference can be passed, but it doesn't suit our needs as we either need to return a user if it is present in our store or show her absence. Besides being used as a return type of a method, Optional can be used as a type of a method's parameter and Dropwizard allows us to pass values of such type to resource methods.
Instead of using the @Default annotation,
public String getTailoredGreetingWithQueryParam(@DefaultValue("world")
@QueryParam("name") String name)
one can use an argument of Optional type.
public String getTailoredGreetingWithQueryParam(
@QueryParam("name") Optional<String> name) {
if (name.isPresent()) {
return "Hello " + name.get();
} else {
return "Hello world";
}
//The same can be accomplished using or(...) method to provide the default value
//return "Hello " + name.or("world");
}
The code in the snippet above checks whether the value is present in the name object of Optional type and then extracts the value. An attempt to extract a value from an empty Optional leads to an exception. If value is absent, the default is used. The same logic can be implemented using a convenience or(...) method which returns the value from the Optional object if present and the value passed to it as an argument otherwise. Going back to authentication, similar code to check presence of user in the store is used somewhere upstream where the authenticate(...) method is called. Please note, that Java 8 has its own Optional class.
The second step is to register our Authenticator in the run method of the DWGettingStartedApplication class.
@Override
public void run(final DWGettingStartedConfiguration configuration,
final Environment environment) {
environment.jersey().register(AuthFactory.binder(
new BasicAuthFactory<>(
new GreetingAuthenticator(),
"SECURITY REALM",
User.class)));
// Resources are registered here
}
It should be noted that our discussion pertains to 0.8.x version of Dropwizard. The code for the previous version, 0.7.1 can be found in a repository here in a separate branch. The crux of the code above is a BasicAuthFactory class the constructor of which has three parameters: a class implementing the Authenticator interface, an obscure string which should be displayed to an unauthenticated client inside HTTP WWW-Authenticate header and the aforementioned class a reference of which is returned inside an object of Optional type by the authenticate method.
The importance of the BasicAuthFactory class is that it's method provide() defined in org.glassfish.hk2.api.Factory interface, which is used behind the scenes, does all the heavy-lifting concerning authentication. The method extracts the contents of the Authentication header and checks whether the credentials were provided. If not, the DefaultUnauthorizedHandler is used to render the 401 Unauthorized response with message "Credentials are required to access this resource.". Otherwise, the credentials are decoded and extracted and used to create an instance of BasicCredentials class. After that our authenticate method is called to check the validity of credentials. If they are not valid, the described above response is produced, otherwise an instance of our User class is returned.
The final step is to secure our method, that is to instruct Dropwizard that only authenticated users are allowed to access particular resource. This is done with help of @Auth annotation as shown below.
@Path("secured_hello")
public class SecuredHelloResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String getGreeting(@Auth User user) {
return "Hello world!";
}
}
The annotation instructs Dropwizard to ask users to enter credentials every time the resource is accessed or user may supply credentials without prompt and if they are valid, she is granted access. In the process of providing the resource method with an object of the User class the provide() method is called. It should be noted that the resource should be registered in the DWGettingStartedApplication class to become accessible. The process of interaction with a secured resource is shown below.
One can use an i option of cURL to show headers
curl -w "\n" -i localhost:8080/secured_hello
or F12 tools of a browser. When a secured resource is accessed using a browser, a dialog prompting to enter credentials is shown.
If one presses Cancel, it is possible to inspect headers.
It is seen both in the terminal and in the browser that the aforementioned obscure string is used as the name of the realm in the response. The realm to security is what a package to classes in Java, the way to group user and assign a storage type for them. If one enters credentials in the browser, they are cached in order to prevent the user to enter them each time the resource is hit, and then the page is displayed.
Going back to cURL, while entering credentials as a content of a header is a viable option, there is a u-option which allows to enter column-separated user-ID:password pair and accomplishes encoding for us.
curl -w "\n" -u javaeeeee:crimson localhost:8080/secured_hello
The result “Hello world! ” without quotes should be displayed in the terminal.
To use Postman to access secured resource on should use the Basic Auth tab, where credentials are set.
After that the Refresh headers button should be pressed and one can enter the URL and press the Send button.
The response appears in the window.
Configuration
Now let's talk about configuration. A more complex API needs a database connection so it is necessary that connection parameters be stored somewhere. Dropwizard uses YAML (YAML Ain't Markup Language) human-readable data format to store configuration. The configuration is deserialized into an instance of a class that extends io.dropwizard.Configuration in the case of our project it is DWGettingStartedConfiguration.
It should be noted that there are some build-in parameters, for example the port we use to connect to our application, and such parameters have some sensible defaults, like the port is preset to 8080. A developer can use her own parameters and for the sake of learning how to use user-defined parameters we'll add user-ID and password to our configuration file and use them for authentication. Later we'll see how to use configuration to enable encryption.
Let's create a configuration file. It's name is not important as it is passed as a command-line argument and we can call it config.xml. The file should be placed in the project's folder. The snippet is shown below.
## Configuration file for DWGettingStarted application.
---
# User login.
login: javaeeeee
# User password.
password: crimson
We added two fields login and password; the keys and values are separated with a colon and a space. The hash sign is used for comments. These two configuration parameters can be read into the fields of a configuration class using Jersey.
public class DWGettingStartedConfiguration extends Configuration {
@NotNull
private String login;
@NotNull
private String password;
@JsonProperty
public String getLogin() {
return login;
}
@JsonProperty
public String getPassword() {
return password;
}
}
The @JsonProperty annotations on getters are used for deserialisation of the configuration file. If one places them also on setters, the configuration values from the object can be written to the file or serialized. @NotNull annotations on the fields are part of Hibernate Validator and are used to check that the fields are not null, that is if there is no corresponding values in the configuration file, the application will not start.
To use the settings we should change our Authenticator as shown below.
public class GreetingAuthenticator
implements Authenticator<BasicCredentials, User> {
private String login;
private String password;
public GreetingAuthenticator(String login, String password) {
this.login = login;
this.password = password;
}
@Override
public Optional<User> authenticate(BasicCredentials credentials)
throws AuthenticationException {
if (password.equals(credentials.getPassword())
&& login.equals(credentials.getUsername())) {
return Optional.of(new User());
} else {
return Optional.absent();
}
}
}
Also we need to pass arguments to the Authenticator in the process of registration. The run(...) method of the DWGettingStartedApplication class has our configuration class as the parameter, so we can easily extract our credentials.
public void run(final DWGettingStartedConfiguration configuration,
final Environment environment) {
environment.jersey().register(AuthFactory.binder(
new BasicAuthFactory<>(
new GreetingAuthenticator(configuration.getLogin(),
configuration.getPassword()),
"SECURITY REALM",
User.class)));
// ...
}
To start the application which uses the configuration file one should type in the terminal the following command
java -jar target/DWGettingStarted-1.0-SNAPSHOT.jar server config.yml
or instruct the IDE to pass the name of the configuration file as an argument.
In addition to the possibility to add one's own configuration parameters, one can change the defaults. For example, to set the port to 8085 on can add to the config.yml the lines shown below.
#Server configuration.
server:
applicationConnectors:
- type: http
port: 8085
HTTPS
HTTPS is a protocol used for secure communication over a network as it relies on Transport Layer Security (TLS) for providing cryptographic protection of the data being transmitted. The predecessor of TLS was called SSL. In fact, HTTPS relies on public key certificates in order to verify the identity of the server and to encrypt all the data transmitted between client and server in order to prevent eavesdropping.
There is a special third party called Certificate Authority (CA) which issues certificates and vouches for their validity, and browsers know how to check the validity of the certificate produced by the server. Actually, the certificate binds the distinguished name of the server to its public key and is produced when client connects to the server and helps to prove to the client that the server is really who it is and not an impostor.
For development and testing purposes one could create a certificate using some special tool, although such certificate is not underwritten by any CA and called a self-signed certificate. This certificate can be used for data encryption but fails the validity check, and the browser will display a warning as the result, which can be ignored by the user. There is no need to know all the nuts and bolts of the HTTPS and TLS to get started, it is possible to rely on a tool to generate all the necessary entities to make it work.
To enable HTTPS one should create a keystore containing a public/private key pair, which can be accomplished by using keytool, a program that is a part of JDK. The following command should be issued from the project's folder; Java 8 is used.
keytool -genkeypair
-keyalg RSA
-dname "CN=localhost"
-keystore dwstart.keystore
-keypass crimson
-storepass crimson
The command was formatted for clarity but should be entered as a single line in a terminal window. The genkeypair option instructs the keytool to generate the pair, keyalg value is an algorithm used to generate the pair, the value of dname option is a distinguished name of the entity for which the certificate is being generated and is a part of the certificate. The keystore option specifies the name of the generated file and the last two options include passwords for the private key and the keystore, keypass and storepass respectively. By default the generated certificate is valid for 90 days, to change this one can use a validity option to specify the expiration period in days. It should be noted that to set the passwords in the command line is a bad practice as those can easily be seen by a third party, but this was done to prevent further questions from the keytool; if omitted, one will be prompted to enter the passwords.
After the keystore was generated it's time to inform Dropwizard about its name and password. This is done using the aforementioned configuration file.
#Server configuration.
server:
applicationConnectors:
- type: http
port: 8080
- type: https
port: 8443
keyStorePath: dwstart.keystore
keyStorePassword: crimson
validateCerts: false
The last parameter allows Dropwizard to start if certificate is not valid, for example if it has expired. Now we are ready to access the resource via an encrypted connection. To do so let's start with cURL.
curl -w "\n" -k -u javaeeeee:crimson https://localhost:8443/secured_hello
The k option switches off certificate validation, that is allows us to work with a self-signed certificate. When trying to connect to a server that uses a self-signed certificate browsers warn you about insecure connection. The Postman Chrome extension will work without any rants.
We have an application which uses various parts of Dropwizard cooperating, namely authentication and configuration and we should use integration tests to check that our application works correctly. To do so we can use the jUnit @ClassRule annotation to start up the application and set the path to its configuration file as the snippet below shows.
public class IntegrationTest {
@ClassRule
public static final DropwizardAppRule<DWGettingStartedConfiguration> RULE
= new DropwizardAppRule<>(DWGettingStartedApplication.class,
"config.yml");
@Test
public void testGetGreeting() {
String expected = "Hello world!";
//Obtain client
Client client = ClientBuilder.newClient();
//Build a feature in basic authentication mode
HttpAuthenticationFeature feature
= HttpAuthenticationFeature.basic("javaeeeee", "crimson");
//Register the feature
client.register(feature);
//Get actual resul string
String actual = client
.target("http://localhost:8080/secured_hello")
.request(MediaType.TEXT_PLAIN)
.get(String.class);
//Do an assertion
assertEquals(expected, actual);
}
}
First, we created a client instance using a JAX-RS API method. Then to use Basic Authentication we built a feature passing credentials and registered it. Finally, we checked that the correct value was returned. To test the sad path one could provide wrong credentials or omit feature registration altogether and check that the response code is 401 (Unauthorized).
To test the resource using HTTPS it is necessary that some additional steps be made as the following snippet shows.
//Create SSL Configurator
SslConfigurator sslConfigurator = SslConfigurator.newInstance();
//Register a keystore
sslConfigurator.trustStoreFile("dwstart.keystore")
.trustStorePassword("crimson");
//Create SSL Context
SSLContext sSLContext = sslConfigurator.createSSLContext();
//Obtain client
Client client = ClientBuilder
.newBuilder()
.sslContext(sSLContext)
.build();
First, one should create an instance of SslConfigurator utility class and set our key store as a trust store for the client and set the password for the key store. This would make the client to trust the certificate, otherwise you'll get an exception. Second, SSLContext is created and set to ClientBuilder. Finally, you have an instance of a Jersey 2.x client which is able to connect to your application via HTTPS. Other steps of the test are the same.
References
Opinions expressed by DZone contributors are their own.
Comments