The Java EE Security API is here!

This is the third day of the Java EE Advent Calendar. Yesterday, HASUNUMA Kenji's [Presentation] Introduction to JCA and MDB (ja) was.

Introduction

Well, Java EE 8 has finally been released this year! Is CDI 2.0 or Servlet 4 the highlight? I'm personally interested in this area because someone will explain it Java EE Security API (JSR 375) -spec.html) I would like to talk about.

The sample used this time can be obtained from the following. https://github.com/koduki/example-javaee8-security_basic

What is it in the first place?

The Java EE Security API (JSR 375), as the name implies, is a specification that makes security, especially authentication and authorization, simpler and more portable. Most web applications have login capabilities and account access control. It doesn't matter who makes the functions around here, so I want FW to do it, right? With Rails, Devise provides such a function, and PHP's Symfony was incorporated as a standard function of FW.

However, although JACC, JASPIC, and vendor-specific implementations have existed in JavaEE for some time, they are complicated and there are few documents due to their own specifications, so the reality is that most people have implemented their own authentication / authorization. I suspect there isn't. In addition, since security was implemented independently by various components such as Servlet, JSF, CDI, EJB, etc., there was no specification for integrated management of it.

Taking these circumstances into consideration, JSR375 was created with reference to OSS libraries such as Apache Shiro with the keywords "unified management", "simple", and "portable". It is a specification.

What can i do?

The main points are the following three points.

--Unified access to each component --Access control by annotation --Unified access to authentication function

Unified access to each component

First, a Security Context was introduced to unify the authentication functions that were previously independent of each other. For example, until now, there were the following methods for acquiring an account for each component.

Access to these is summarized in the SecurityContext. The method I / F of each existing process remains the same, but the backend etc. seems to have changed. Also, Java EE 8 is totally integrated into CDI, so even if it's a serve red, for example.

public class MyServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String webName = request.getUserPrincipal().getName();

The part that confirmed the login user as

public class MyServlet extends HttpServlet {
    @Inject
    private SecurityContext securityContext;

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String webName = securityContext.getCallerPrincipal().getName();

And can also be taken via SecurityContext.

Access control by annotation

Access control can be easily performed by annotation. For example, a servlet that can only be accessed by users with the foo role can be created as follows.

@ServletSecurity(@HttpConstraint(rolesAllowed = "foo"))
public class Servlet extends HttpServlet {

If Authentication Mechanism described later is set properly, you can jump to the login page by just setting the above, and you can easily define the flow after authentication or error.

Unified access to authentication function

The third point is unified access to the authentication function. We provide this in the form of an Identity Store. You can use DB, LDAP, or custom authentication function.

New web applications will often have an RDB as the back end, and enterprise systems will often use LDAP such as Active Directory. You can use these two just by writing the settings with annotations. Since EL expressions can be used for annotations, not only hard coding but also modern usage such as acquisition from configuration files and environment variables can be done without problems.

Also, you often want to use your own authentication API or standard authentication APIs such as OAuth and SAML. Even in that case, you can handle it by creating a custom Identity Store or Authentication Mechanism. In fact, the article "OpenID Connect with Java EE 8 Security API" There is also a case where OpenID Connect is supported, which is authenticated with relatively complicated transitions. It seems that the in-house API can usually be supported by referring to this. As mentioned in the article, I would like support for OAutht and other general authentication functions as standard or close to it.

Component

Although the explanation is a little duplicated, the following three components are mainly used directly.

Authentication Mechanism

The Authentication Mechanism controls the authentication and subsequent control flow. It seems that BASIC authentication will be performed with @BasicAuthenticationMechanismDefinition, which is mainly provided, or HttpAuthenticationMechanism will be inherited and custom forms will be used. By combining HttpAuthenticationMechanism and @LoginToContinue etc., I feel that general login control can be created quickly.

The simple in-house system and API are BASIC authentication, and if you need a login screen, you can use a custom form.

Identity Store

The Identity Store provides the ability to authenticate.

For example, if you want the backend to be an RDB, you can use @DatabaseIdentityStoreDefinition and define it as follows:

@DatabaseIdentityStoreDefinition(
        dataSourceLookup = "${Config.getDataSourceName()}", // "java:global/MyAppDataSource"
        callerQuery = "select password from caller where name = ?",
        groupsQuery = "select group_name from caller_groups where caller_name = ?",
        hashAlgorithm = Pbkdf2PasswordHash.class,
        priorityExpression = "#{100}",
        hashAlgorithmParameters = {
            "Pbkdf2PasswordHash.Iterations=3072",
            "${applicationConfig.dyna}"
        } // just for test / example
)

Since you can also write an EL expression in the annotation, you can also create a configuration file or a class to be read from the outside and get the configuration value from it as in this example.

SecurityContext

SecurityContext is a mechanism for unified access to the above two functions. As mentioned above, the methods of accessing the authentication mechanism are diverse and complicated. SecurityContext makes it easy to:

--getCallerPrincipal: Get the Caller (user). Get as a String with getName --isCallerInRole: Check if the role passed as an argument has a logged-in one

Implementation example

Let's show a super-simple implementation example. This time with BASIC authentication.

First, install GlassFish 5 and work with NetBeans. If you create a Maven project with NetBeans 8.2, it will be EE7, so let's modify it to EE8. By the way, Java is also specified in 1.8.

@@ -18,7 +18,7 @@
         <dependency>
             <groupId>javax</groupId>
             <artifactId>javaee-web-api</artifactId>
-            <version>7.0</version>
+            <version>8.0</version>
             <scope>provided</scope>
         </dependency>
     </dependencies>
@@ -30,8 +30,8 @@
                 <artifactId>maven-compiler-plugin</artifactId>
                 <version>3.1</version>
                 <configuration>
-                    <source>1.7</source>
-                    <target>1.7</target>
+                    <source>1.8</source>
+                    <target>1.8</target>
                     <compilerArguments>
                         <endorseddirs>${endorsed.dir}</endorseddirs>
                     </compilerArguments>

First, make a target serve red. I will omit the description, but also create beans.xml and ApplicationConfig.java empty

@WebServlet("/myservlet")
public class MyServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    @Inject
    private SecurityContext securityContext;

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        response.getWriter().write("This is a servlet \n");

        String webName = null;
        if (securityContext.getCallerPrincipal() != null) {
            webName = securityContext.getCallerPrincipal().getName();
        }
        response.getWriter().write("web username: " + webName + "\n");

        response.getWriter().write("web user has role \"admin\": " + securityContext.isCallerInRole("admin") + "\n");
        response.getWriter().write("web user has role \"users\": " + securityContext.isCallerInRole("users") + "\n");
        response.getWriter().write("web user has role \"guest\": " + securityContext.isCallerInRole("guest") + "\n");
    }

}

When accessed, the result will be as follows.

$ curl -L http://localhost:8080/example-javaee8-security_basic/myservlet
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   137  100   137    0     0   9133      0 --:--:-- --:--:-- --:--:--  9133
This is a servlet
web username: null
web user has role "admin": false
web user has role "users": false
web user has role "guest": false

BASIC authentication

First, make this page accessible only to Admin. Specify @ServletSecurity for the serve red to limit the role.

@WebServlet("/myservlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "admin"))
public class MyServlet extends HttpServlet {
.
.
.

If you try to access it again, you can see that it resulted in a 401 error.

$ curl -IL http://localhost:8080/example-javaee8-security_basic/myservlet
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0  1090    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
HTTP/1.1 401 Unauthorized
Server: GlassFish Server Open Source Edition  5.0

Since you cannot access it as it is, add BASIC authentication. Add @BasicAuthenticationMechanismDefinition to ApplicationConfig.java.

@BasicAuthenticationMechanismDefinition(
        realmName = "test realm"
)

@ApplicationScoped
@Named
public class ApplicationConfig {

}

We will also create an Identity Store that will be the back end for the Basic Authentication Mechanism Definition.

@ApplicationScoped
public class TestIdentityStore implements IdentityStore {
    public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
        if (usernamePasswordCredential.compareTo("admin", "password")) {
            return new CredentialValidationResult("admin", new HashSet<>(asList("admin", "users")));
        }
        return INVALID_RESULT;
    }
}

The password is also a hard-coded super-simple specification. A good girl shouldn't imitate in production, right? Let's deploy and run it.

$ curl -LI -u admin:password http://localhost:8080/example-javaee8-security_basic/myservlet
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0   137    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
HTTP/1.1 200 OK
Server: GlassFish Server Open Source Edition  5.0

$ curl -u admin:password http://localhost:8080/example-javaee8-security_basic/myservlet
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   137  100   137    0     0   4419      0 --:--:-- --:--:-- --:--:--  8562
This is a servlet
web username: admin
web user has role "admin": true
web user has role "users": false
web user has role "guest": false

You can see that the result is returned properly without becoming 401. You can see the result of securityContext properly.

Use of database

Hard coding is not so much, so let's change the backend to RDB. First of all, you need a database to use. If you are using GlassFish5, Derby will be included, so use that. Initialize the DB with the following code and create a data source.

Originally, you should make a management tool, but this time the password is also hashed and stored. I also changed the password from the previous one to make it easier to verify.

@DataSourceDefinition(
        name = "java:global/MyAppDataSource",
        minPoolSize = 0,
        initialPoolSize = 0,
        className = "org.apache.derby.jdbc.ClientDataSource",
        user = "APP",
        password = "APP",
        databaseName = "myapp",
        properties = {"connectionAttributes=;create=true"}
)
@Singleton
@Startup
public class DatabaseSetup {

    @Resource(lookup = "java:global/MyAppDataSource")
    private DataSource dataSource;

    @Inject
    private Pbkdf2PasswordHash passwordHash;

    @PostConstruct
    public void init() {

        Map<String, String> parameters = new HashMap<>();
        parameters.put("Pbkdf2PasswordHash.Iterations", "3072");
        parameters.put("Pbkdf2PasswordHash.Algorithm", "PBKDF2WithHmacSHA512");
        parameters.put("Pbkdf2PasswordHash.SaltSizeBytes", "64");
        passwordHash.initialize(parameters);

//        executeUpdate(dataSource, "DROP TABLE caller");
//        executeUpdate(dataSource, "DROP TABLE caller_groups");
        executeUpdate(dataSource, "CREATE TABLE caller(name VARCHAR(64) PRIMARY KEY, password VARCHAR(255))");
        executeUpdate(dataSource, "CREATE TABLE  caller_groups(caller_name VARCHAR(64), group_name VARCHAR(64))");

        executeUpdate(dataSource, "INSERT INTO caller VALUES('admin', '" + passwordHash.generate("secret1".toCharArray()) + "')");

        executeUpdate(dataSource, "INSERT INTO caller_groups VALUES('admin', 'admin')");
    }

    @PreDestroy
    public void destroy() {
        try {
            executeUpdate(dataSource, "DROP TABLE caller");
            executeUpdate(dataSource, "DROP TABLE caller_groups");
        } catch (Exception ex) {
            ex.printStackTrace();
            // silently ignore, concerns in-memory database
        }
    }

    private void executeUpdate(DataSource dataSource, String query) {
        try (Connection connection = dataSource.getConnection()) {
            try (PreparedStatement statement = connection.prepareStatement(query)) {
                statement.executeUpdate();
            }
        } catch (SQLException ex) {
            throw new IllegalStateException(ex);
        }
    }

}

Next, delete the TestIdentityStore created earlier and add @DatabaseIdentityStoreDefinition to ApplicationConfig.java instead.

@BasicAuthenticationMechanismDefinition(
        realmName = "test realm"
)
@DatabaseIdentityStoreDefinition(
        dataSourceLookup = "java:global/MyAppDataSource",
        callerQuery = "select password from caller where name = ?",
        groupsQuery = "select group_name from caller_groups where caller_name = ?",
        hashAlgorithm = Pbkdf2PasswordHash.class,
        priorityExpression = "#{100}",
        hashAlgorithmParameters = {
            "Pbkdf2PasswordHash.Iterations=3072",
            "${applicationConfig.dyna}"
        } // just for test / example
)
@ApplicationScoped
@Named
public class ApplicationConfig {

    public String[] getDyna() {
        return new String[]{"Pbkdf2PasswordHash.Algorithm=PBKDF2WithHmacSHA512", "Pbkdf2PasswordHash.SaltSizeBytes=64"};
    }
}

The data source used is the one created in DatabaseSetup.java earlier. The password hash algorithm also uses PBKDF2 so that the received password has the same hash value as that registered in the DB.

I will try this.

$ curl -u admin:secret1 http://localhost:8080/example-javaee8-security_basic/myservlet
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   137  100   137    0     0   4419      0 --:--:-- --:--:-- --:--:--  4419
This is a servlet
web username: admin
web user has role "admin": true
web user has role "users": false
web user has role "guest": false

Did you authenticate with the new password registered in the DB? Since the back end of the authentication mechanism is separated in this way, the implementation can be easily changed.

Summary

There are many places I haven't investigated yet, but I tried to touch Java EE Security API (JSR 375). It was. If it is a simple API authentication function, it is convenient because it seems that even today's contents can be created quickly.

In the future, I would like to try more practical parts such as LDAP linkage and custom form linkage to make an article.

Then Happy Hacking!

reference

Recommended Posts

The Java EE Security API is here!
What is the difference between Java EE and Jakarta EE?
Java is the 5th day
[java8] To understand the Stream API
Where is the Java LocalDateTime.now () timezone?
Parsing the COTOHA API in Java
Try using the Stream API in Java
Call the Windows Notification API in Java
Hit the Salesforce REST API from Java
What is the best file reading (Java)
What is the main method in Java?
What is java
ChatWork4j for using the ChatWork API in Java
What is the Java Servlet / JSP MVC model?
The order of Java method modifiers is fixed
The intersection type introduced in Java 10 is amazing (?)
Java Stream API
What is Java <>?
What is the volatile modifier for Java variables?
What is Java
Try using the COTOHA API parsing in Java
[Java] Something is displayed as "-0.0" in the output
[Java] The word passing by reference is bad!
The comparison of enums is ==, and equals is good [Java]
Use Java lambda expressions outside of the Stream API
Which is better, Kotlin or Java in the future?
[JAVA] What is the difference between interface and abstract? ?? ??
The story that .java is also built in Unity 2018
Pack API response (java)
[Java] Stream API / map
Docker-Client Java API Troubleshooting
What is Java Encapsulation?
Java8 Stream API practice
Zabbix API in Java
What is Java technology?
What is Java API-java
[Java] What is flatMap?
[Java] What is JavaBeans?
[Java] What is ArrayList?
What is the LocalDateTime class? [Java beginner] -Date and time class-
[Java] "T" is included in date type JSON in API response
I want to use the Java 8 DateTime API slowly (now)
[Java] [Play Framework] Until the project is started with Gradle
[Java] Note the case where equals is false but == is ture.
[Java] com.sun.glass.WindowEvent is imported and the window does not close
Java: The problem of which is faster, stream or loop
Now is the time to get started with the Stream API
How to play MIDI files using the Java Sound API