This is second part of series dedicated to Apache Shiro. We started previous part with simple unsecured web application. When we finished, the application had basic authentication and authorization. Users could log in and log out. All web pages and buttons had access rights assigned and enforced. Both authorization and authentication data have been stored in static configuration file.
As we promised in the end of last part, we will move user account data to database. In addition, we will give users an option to authenticate themselves via PGP certificates. As a result, our application will have multiple alternative log in options: log in with user name/password and log in with certificate. We will finish by turning alternative log in options mandatory.
version that requires both user name/passwords and PGP Certificates for log-in.
Each version has test class RunWaitTest. The class starts web server with application deployed at http://localhost:9180/simpleshirosecuredapplication/ url.
Note: We updated previous part since first release. The most notable change is new section which shows how to add error message to login page. Thanks everybody for feedback.
First, we explain what realms are and how to create them. If you are not interested in theory, proceed to next chapter.
Realms are responsible for authentication and authorization. Any time user wants to log in to the application, authentication information is collected and passed to realm. Realm verifies supplied data and decides whether user should be allowed to log in, have access to resource or own specific role.
credential – proves users identity e.g. password, PGP certificate, fingerprint, … .
All realms implement Realm interface. Two interface methods are important: supports and getAuthenticationInfo. Both receive principal and credentials inside authentication token object.
Supports method decides whether realm is able to authenticate user based on supplied authentication token. For example, if my realm checks user name and password, it rejects authentication token with X509 certificate only.
Method getAuthenticationInfo performs authentication itself. If principal and credential from authentication token represents valid log in information, the method returns authentication info object. otherwise, the realm returns null.
If the realm wishes to do also authorization, it has to implement Authorizer interface. Each Authorizer method takes principal as parameter and checks either role(s) or permission(s). It is important to understand, that the realm obtains all authorization requests, even if they came from user authenticated by another realm. Of course, realm may decide to ignore any authorization request.
Permissions are supplied either as strings or as permission objects. Unless you have strong reason to do otherwise, use WildcardPermissionResolver to convert strings into permission objects.
configured converter between permission strings and permission objects.
These features are available to any realm that implements additional interface. No other configuration is necessary.
The easiest way to create new realm is to extend either AuthenticatingRealm or AuthorizingRealm class. They have reasonable implementation of all useful interfaces mentioned in previous section. If they are not usable for your needs, you can extend CachingRealm or create new realm from scratch.
Current version of SimpleShiroSecuredApplication uses default realm for both authentication and authorization. Default realm – IniRealm reads user account information from configuration file. Such storage is acceptable only for simplest applications. Anything slightly more complex needs to have credentials stored in better persistent storage.
New requirements: account credentials and access rights are stored in database. Stored passwords are hashed and salted.
In this chapter, we will connect application to database and create tables to store all user account data. Then, we will replace IniRealm with realm able to read from database and salt passwords.
The section describes sample application infrastructure. It contains no information about Shiro, so you can freely skip it.
Example application uses Apache Derby database in embedded mode.
We use Liquibase for database deployment and upgrades. It is open source library for tracking, managing and applying database changes. Database changes (new tables, new columns, foreign keys) are stored in database changelog file. Upon start up, Liquibase investigates database and apply all new changes. As a result, database is always consistent and up to date with no real effort on our part.
Finally, jetty configured to read jetty-web.xml with enabled jndi is in AbstractContainerTest class.
It is not able to load data source from JNDI (open issue).
It is not able to salt passwords (open issue).
Method doGetAuthenticationInfo reads account authentication information from the database and converts it into authentication info object. It returns null if no account information is found. Parent class AuthenticatingRealm compares authentication info object with original user supplied data.
The example here contains only most important piece of code. Full class is available on Github.
saltedJdbcRealm.authenticationQuery = select password, salt from sec_users where name = ?
saltedJdbcRealm.userRolesQuery = select role_name from sec_users_roles where user_name = ?
saltedJdbcRealm.permissionsQuery = select permission from sec_roles_permissions where role_name = ?
NOTE: we removed sections [users] and [roles] from configuration file. Otherwise, Shiro would use both IniRealm and JdbcRealm. This would create multi-realm scenario which is out of scope of this chapter.
From user point of view, application works exactly the same way as before. He can log in to the same user accounts as before. However, user names, passwords, salts, permissions and roles are now stored in database.
Full source code is available in 'authentication_stored_in_database' branch on Github.
Some systems allow multiple authentication means for user log in. For example, user may supply user name/password, log in with Google account, Facebook account or anything else. We will add something similar to our simple application. We will give our users option to authenticate themselves with PGP certificates.
New requirements: application supports PGP certificates as alternative authentication mechanism. Login screen shows up only if user does not possess valid certificate associated with application account. If the user has valid known PGP certificate, he is logged automatically.
When user tries to log in to the application, he has to supply authentication data. Those data are captured by servlet filter. Filter converts data to authentication token and pass the token to realms. If any realm wish to authenticate user, it converts authentication token to authentication info object. If the realm does not wish to do so, then it returns null.
realm to validate certificates and match them to user accounts.
Our application will have two different realms. One uses names to identify accounts and passwords to authenticate users, other uses PGP certificates to do both.
When user visits web application, his web browser may send copy of PGP certificate to web server. The certificate is signed either by some certificate authority or by the certificate itself (self-signed certificate). Web server keeps list of certificates it trusts to in storage called truststore. If the truststore contains either users certificate or certificate of authority that signed it, then web server trusts users certificate. Trusted certificates are passed to the application.
associate certificates with user accounts.
Create and manage certificates in portecle. Sample certificates for SimpleShiroSecuredApplication are located in src\test\resources\clients directory. All stores and certificates have common password 'secret'.
Create new jks keystore: File -> New Keystore, select jks.
Generate new certificate: Tools -> Generate Key Pair. Leave password fields empty, certificate will inherit password from keystore.
Export public certificate: select new certificate -> right click -> Export, select Head Certificate. This creates .cer file.
Export private key and certificate: select new certificate -> right click -> Export, select Private Key and Certificates. This creates .p12 file.
.cer file contains only public certificate, so you can give it to anybody. On the other hand, .p12 file contains users private key and must be kept secret. Distribute it only to user (e.g. import it to your browser for testing).
File -> New Keystore, select jks.
Tools -> Import Trusted Certificate.
Web server has to ask for certificates and validate them against truststore. It is not possible to ask for certificate from Java. Each web server is configured differently. Jetty configuration is available in Look at AbstractContainerTest class on Github.
Each certificate is uniquely identified by serial number and name of certificate authority that signed it. We store them together with user names and passwords in database table. Database changes are in db.changelog.xml file, see changeset 3 for new columns and changeset 4 for data initialization.
Authentication token represents user data and credentials during an authentication attempt. It must implement authentication token interface and holds whatever data we wish to pass between servlet filter and realm.
Shiro filter converts user data to authentication tokens. Up to now, we used FormAuthenticationFilter. If incoming request comes from logged user, the filter lets user in. If the user is trying to authenticate himself, the filter creates authentication token and pass it to the framework. Otherwise it redirects user to the login screen.
Our filter CertificateOrFormAuthenticationFilter extends FormAuthenticationFilter.
First, we have to convince it that not only requests with user name and passwords, but also any request with PGP certificate can be considered a log in attempt. Second, we have to modify the filter to send PGP certificate along with user name and password in an authentication token.
Our new realm will be responsible only for authentication. Authorization (access rights) will be handled by JNDIAndSaltAwareJdbcRealm. Such configuration works as long as PGP certificate authenticates user into the same account as user name/password would. Otherwise said, primary principal returned by new realm must be the same as primary principal returned by JNDIAndSaltAwareJdbcRealm.
Our realm does not need caching nor any other service provided by optional interfaces. Therefore, we have to implement only two interfaces: Realm and Nameable.
Note that the realm has two new properties: trustStore and trustStorePassword. Both are needed for PGP certificate validation. As any other property, both are configurable in configuration file.
It is now possible to log in to the application with PGP certificate. If the certificate is not available, user name and password works too.
Application source code is available in 'certificates_as_alternative_log_in_method' branch on Github.
By default, 'at least one successful strategy' is used, which suits our purposes well. Again, it is possible to create custom authentication strategy. For example, we may require user to present both PGP certificate and user name/password credentials to log in.
New requirements: user has to present both PGP certificate and user name/password credentials to log in.
fails if two realms authenticate different principals.
If you run the application now, both certificate and user name/password log in methods are mandatory.
This version is available in 'certificates_as_mandatory_log_in_method' branch on Github.
This part was dedicated to Shiro realms. We created three different application versions all of them are available on Github. They covered basic and probably most important realm features.
If you need to know more, start with classes linked here and read their javadocs. They are well written and extensive.
Reference: Apache Shiro Part 2 – Realms, Database and PGP Certificates from our JCG partner Maria Jurcovicova at the This is Stuff blog.
Thanks, Maria, for such a comprehensive and helpful post on how to handle a custom realm as well as multiple realms. This is a topic I wish Shiro had better documentation for. This information was just what I needed.