September 16, 2019
This article shows how to implement BASIC authentication and role-based access control using the new security subsystem in WildFly, Elytron. The app to be secured is a RESTful web service. GET methods querying data are public while the data-changing methods (POST, PUT, and DELETE) require an admin role. The implementation uses the PostgresSQL database as a credentials store.
Declarative role-based access control for JavaEE web artifacts is set up in the web.xml file. In this demonstration, all of the deployed JAX-RS service calls will be verified against this constraint if they are POST, PUT, or DELETE. These methods require an admin role.
The following web.xml file defines the resource under scrutiny broadly. All requests -- URLs matching /* -- are checked against the list of HTTP methods. If there is a method match, WildFly insists on an admin role. The security roles for the app are then defined using the security-role tags. The login-config specifies that BASIC authentication is to be used.
<?xml version="1.0"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <security-constraint> <web-resource-collection> <web-resource-name>Secured</web-resource-name> <url-pattern>/*</url-pattern> <http-method>POST</http-method> <http-method>PUT</http-method> <http-method>DELETE</http-method> </web-resource-collection> <auth-constraint> <role-name>admin</role-name> </auth-constraint> </security-constraint> <security-role> <description>The role that is required to log operate data-changing services</description> <role-name>admin</role-name> </security-role> <login-config> <auth-method>BASIC</auth-method> </login-config> </web-app>
Some documentation points to adding a security-domain element to the jboss-web.xml. This article will apply the same security domain to all deployed artifacts through a setting described later. If you needed to use multiple domains on different WAR artifacts, add a security-domain element to the jboss-web.xml file.
It's critical that SSL be used in this deployment since BASIC authentication will send the un-encrypted credentials in an Authorization header.
From a developer's perspective, that's the only change that needs to be made to a webapp to secure it. In the real world, there would likely be some security on the GET methods. Additional requirements can be implemented by defining a URL structure (say "/private" or "/secured" or "/admin" for non-public services). Custom filters can also be used for finer-grain access that allow a GET method, but otherwise restrict the data.
There are many different stores that can provide user and role information to Elytron. This article covers a JDBC Realm which will hold user information, credentials, and roles in a relational database. The database used is PostgresSQL.
You have a lot of flexibility in defining the physical model supporting Elytron. I've opted for a many-to-many user / role model. This could be condensed to a single table or hidden behind a view. In the following model, a user table wildfly_user is joined to roles in wildfly_role through a join table wildfly_user_role. IDs are generated through PostgresSQL sequences.
These are scripts generated from the above model. I'm not actually using referential integrity as in the diagram.
CREATE TABLE wildfly_user ( wildfly_user_id bigint NOT NULL, username varchar(50) NOT NULL, password varchar(100) NULL ) ; ALTER TABLE wildfly_user ADD CONSTRAINT "PK_wildfly_user" PRIMARY KEY (wildfly_user_id) ; CREATE TABLE wildfy_role ( wildfly_role_id bigint NOT NULL, role_name varchar(50) NOT NULL, wildfly_user_role_id bigint NULL ) ; ALTER TABLE wildfy_role ADD CONSTRAINT "PK_wildfy_role" PRIMARY KEY (wildfly_role_id) ; CREATE TABLE wildfly_user_role ( wildfly_user_role_id bigint NOT NULL, wildfly_user_id bigint NOT NULL, wildfly_role_id bigint NOT NULL ) ; ALTER TABLE wildfly_user_role ADD CONSTRAINT "PK_wildfly_user_role" PRIMARY KEY (wildfly_user_role_id) ;
Although WildFly does support passwords stored as plain text, passwords should always be hashed. I've installed the pgcrypto extension to that I can use a hashing function and an encoding function. Run the following command when connected to PostgresSQL and target database as the superuser.
> create extension pgcrypto
This will give you the ability to execute the digest() and the encode() function. In this article, I used the sha-256 hashing algorithm. Anything lesser such as MD5 does not give adequate protection. I then convert the binary data to text using the encode() function with a "hex" argument. This will be referenced in the WildFly JDBC Realm configuration.
Here's sample DML for adding a user to the wildfly_user table. This user has the role admin. The join table is using values assuming that this was the first inserts on wildfly_user and wildfly_role. If there were records added already, you'll need to adjust the "1, 1" arguments to wildfly_user_role.
INSERT INTO wildfly_user VALUES (nextval('wildfly_user_id_seq'), 'myuser', digest('p@ssw0rd50960170411_da!', 'sha256')); INSERT INTO wildfly_role VALUES (nextval('wildfly_role_id_seq'), 'admin') INSERT INTO wildfly_user_role VALUES (nextval('wildfly_user_role_id_seq'), 1, 1)
At this point, the PostgresSQL contains a single user record that can be joined to an admin role record through the join table, wildfly_user_role.
These next steps work directly with the Elytron subsystem. From Configuration, navigate to the Security Elytron subsystem. Go to Security Realms > Security Realm > JDBC Realms. Press the Add button.
Specify a name, select your PostgresSQL datasource, and add some SQL. The SQL used to support my many-to-many model will be given in two parts. To get through the form, enter the following. The second part will be added later
SELECT password FROM wildfly_user WHERE username=?
This is the SQL supporting authentication. Select the new Principal Query. Go into the lower Simple Digest Mapper tab. Enter the simple-digest-sha-256 with an index of 1. Your version of the console may also allow you to specify "hex" as an encoding. This is the instruction describing which column provides the password and the format of the data.
Press the Add button again to add the SQL supporting authorization. Specify the datasource and enter the following for the SQL.
SELECT role_name FROM wildfly_role r JOIN wildfly_user_role ur ON (r.wildfly_role_id = ur.wildfly_role_id) JOIN wildfly_user u ON (ur.wildfly_user_id = u.wildfly_user_id) WHERE u.username = ?
Under Attributes, press Edit. Type in "groups=1" and press the enter key. Save the result. Your new JDBC Realm should look like the following.
Next, we'll define a Security Domain. From the Elytron subsystem, select Other Settings > View > SSL > Security Domain. You'll navigate through SSL even if you're on your localhost and just working with HTTP. Press Add and name your Security Domain and pair it with the JDBC Realm.
Press Edit and specify default-permission-mapper for Permission Mapper. Save the result. Back at the HTML table, select the Realms button next to the new Security Domain. Specify the Realm and the groups-to-roles Role Decoder.
Next, we'll link the new Security Domain to BASIC Authentication. From the Elytron configuration, select Factories / Transformers > View > HTTP Factories > HTTP Authentication Factory. Press Add.
Specify a name, select the Security Domain from the previous step, and an HTTP Server Mechanism Factory of "global" (the only selection). When that is created, press the Mechanism Configurations button in the HTML for the new entry.
For the Mechanism Name, type "BASIC". After returning to the table, press the Mechanism Realm Configurations button. Press add and type the Realm defined in the JDBC Realms step.
The Elytron subsystem has been configured. To link it to the webapp, we'll configure Undertow next. We'll go to Configuration > Web Undertow > Application Security Domain. Press the plus button.
This will create an Application Security Domain which can be referenced from the jboss-web.xml or specified as a global default. This can abstract the Security Domain created in the previous steps. In our case, we'll tie this domain to the HTTP Authentication Factory. You do not need (and can't) specify both in this screen despite there being two text boxes.
As mentioned earlier, you can specify a security domain in the jboss-web.xml file. This gives you the ability to select different domains for different deployed artifacts. This article sets a default security domain so that all deployed artifacts are secured under the same umbrella of protection. To set this, to to the Web Undertow > Global Settings > View > Edit. Select the Default Security Domain.
Surprisingly, I wasn't able to deploy without also setting the EJB Security Domain. To make this setting, you'll apply the App Security Domain. To the EJB subsystem.
This article showed how to set up Elytron with the PostgresSQL DB so that passwords at rest were hashed and a many-to-many model was supported. From the developer's perspective, Role-Based Access Control couldn't be easier with JavaEE. Mark up a few URLs declaratively and the whole app is secured. Configuring the WildFly security system -- Legacy or Elytron -- can be a challenge. When going through this for the first time, double-check everything such as the queries and the data (no whitespace in passwords for example).
By Carl Walker
President and Principal Consultant of Bekwam, Inc