API de sécurité Jakarta EE 8

1. Vue d'ensemble

L'API de sécurité Jakarta EE 8 est la nouvelle norme et un moyen portable de gérer les problèmes de sécurité dans les conteneurs Java.

Dans cet article, nous examinerons les trois fonctionnalités principales de l'API:

  1. Mécanisme d'authentification HTTP
  2. Magasin d'identité
  3. Contexte de sécurité

Nous allons d'abord comprendre comment configurer les implémentations fournies, puis comment implémenter une implémentation personnalisée.

2. Dépendances de Maven

Pour configurer l'API de sécurité Jakarta EE 8, nous avons besoin d'une implémentation fournie par le serveur ou d'une implémentation explicite.

2.1. Utilisation de l'implémentation serveur

Les serveurs compatibles Jakarta EE 8 fournissent déjà une implémentation pour l'API de sécurité Jakarta EE 8, et nous n'avons donc besoin que de l'artefact Maven de l'API Jakarta EE Web Profile:

  javax javaee-web-api 8.0 provided  

2.2. Utilisation d'une implémentation explicite

Tout d'abord, nous spécifions l'artefact Maven pour l'API de sécurité Jakarta EE 8:

  javax.security.enterprise javax.security.enterprise-api 1.0  

Et puis, nous ajouterons une implémentation, par exemple, Soteria - l'implémentation de référence:

  org.glassfish.soteria javax.security.enterprise 1.0  

3. Mécanisme d'authentification HTTP

Avant Jakarta EE 8, nous avons configuré les mécanismes d'authentification de manière déclarative via le fichier web.xml .

Dans cette version, l'API de sécurité Jakarta EE 8 a conçu la nouvelle interface HttpAuthenticationMechanism en remplacement. Par conséquent, les applications Web peuvent désormais configurer des mécanismes d'authentification en fournissant des implémentations de cette interface.

Heureusement, le conteneur fournit déjà une implémentation pour chacune des trois méthodes d'authentification définies par la spécification Servlet: authentification HTTP de base, authentification basée sur un formulaire et authentification basée sur un formulaire personnalisé.

Il fournit également une annotation pour déclencher chaque implémentation:

  1. @BasicAuthenticationMechanismDefinition
  2. @FormAuthenticationMechanismDefinition
  3. @CustomFormAuthenrticationMechanismDefinition

3.1. Authentification HTTP de base

Comme mentionné ci-dessus, une application Web peut configurer l'authentification HTTP de base simplement en utilisant l' annotation @BasicAuthenticationMechanismDefinition sur un bean CDI :

@BasicAuthenticationMechanismDefinition( realmName = "userRealm") @ApplicationScoped public class AppConfig{}

À ce stade, le conteneur Servlet recherche et instancie l'implémentation fournie de l' interface HttpAuthenticationMechanism .

A la réception d'une demande non autorisée, le conteneur défie le client de fournir des informations d'authentification appropriées via l'en - tête de réponse WWW-Authenticate .

WWW-Authenticate: Basic realm="userRealm"

Le client envoie alors le nom d'utilisateur et le mot de passe, séparés par deux points «:» et encodés en Base64, via l'en- tête de la demande d' autorisation :

//user=baeldung, password=baeldung Authorization: Basic YmFlbGR1bmc6YmFlbGR1bmc= 

Notez que la boîte de dialogue présentée pour fournir les informations d'identification provient du navigateur et non du serveur.

3.2. Authentification HTTP basée sur un formulaire

L' annotation @FormAuthenticationMechanismDefinition déclenche une authentification basée sur un formulaire, comme défini par la spécification Servlet.

Ensuite, nous avons la possibilité de spécifier les pages de connexion et d'erreur ou d'utiliser les pages raisonnables par défaut / login et / login-error :

@FormAuthenticationMechanismDefinition( loginToContinue = @LoginToContinue( loginPage = "/login.html", errorPage = "/login-error.html")) @ApplicationScoped public class AppConfig{}

Suite à l'appel de loginPage, le serveur doit envoyer le formulaire au client:

Le client doit ensuite envoyer le formulaire à un processus d'authentification de support prédéfini fourni par le conteneur.

3.3. Authentification HTTP basée sur un formulaire personnalisé

Une application Web peut déclencher l'implémentation de l'authentification basée sur un formulaire personnalisé à l'aide de l'annotation @CustomFormAuthenticationMechanismDefinition:

@CustomFormAuthenticationMechanismDefinition( loginToContinue = @LoginToContinue(loginPage = "/login.xhtml")) @ApplicationScoped public class AppConfig { }

Mais contrairement à l'authentification par formulaire par défaut, nous configurons une page de connexion personnalisée et invoquons la méthode SecurityContext.authenticate () comme processus d'authentification de sauvegarde.

Jetons également un coup d'œil au support LoginBean , qui contient la logique de connexion:

@Named @RequestScoped public class LoginBean { @Inject private SecurityContext securityContext; @NotNull private String username; @NotNull private String password; public void login() { Credential credential = new UsernamePasswordCredential( username, new Password(password)); AuthenticationStatus status = securityContext .authenticate( getHttpRequestFromFacesContext(), getHttpResponseFromFacesContext(), withParams().credential(credential)); // ... } // ... }

En conséquence d'invoquer la coutume login.xhtml page, le client soumet le formulaire reçu au « LoginBean de connexion () méthode:

//... 

3.4. Mécanisme d'authentification personnalisé

L' interface HttpAuthenticationMechanism définit trois méthodes. Le plus important est le validateRequest () dont nous devons fournir une implémentation.

Le comportement par défaut des deux autres méthodes, secureResponse () et cleanSubject () , est suffisant dans la plupart des cas.

Jetons un coup d'œil à un exemple d'implémentation:

@ApplicationScoped public class CustomAuthentication implements HttpAuthenticationMechanism { @Override public AuthenticationStatus validateRequest( HttpServletRequest request, HttpServletResponse response, HttpMessageContext httpMsgContext) throws AuthenticationException { String username = request.getParameter("username"); String password = response.getParameter("password"); // mocking UserDetail, but in real life, we can obtain it from a database UserDetail userDetail = findByUserNameAndPassword(username, password); if (userDetail != null) { return httpMsgContext.notifyContainerAboutLogin( new CustomPrincipal(userDetail), new HashSet(userDetail.getRoles())); } return httpMsgContext.responseUnauthorized(); } //... }

Here, the implementation provides the business logic of the validation process, but in practice, it's recommended to delegate to the IdentityStore through the IdentityStoreHandler by invoking validate.

We've also annotated the implementation with @ApplicationScoped annotation as we need to make it CDI-enabled.

After a valid verification of the credential, and an eventual retrieving of user roles, the implementation should notify the container then:

HttpMessageContext.notifyContainerAboutLogin(Principal principal, Set groups)

3.5. Enforcing Servlet Security

A web application can enforce security constraints by using the @ServletSecurity annotation on a Servlet implementation:

@WebServlet("/secured") @ServletSecurity( value = @HttpConstraint(rolesAllowed = {"admin_role"}), httpMethodConstraints = { @HttpMethodConstraint( value = "GET", rolesAllowed = {"user_role"}), @HttpMethodConstraint( value = "POST", rolesAllowed = {"admin_role"}) }) public class SecuredServlet extends HttpServlet { }

This annotation has two attributes – httpMethodConstraints and value; httpMethodConstraints is used to specify one or more constraints, each one representing an access control to an HTTP method by a list of allowed roles.

The container will then check, for every url-pattern and HTTP method, if the connected user has the suitable role for accessing the resource.

4. Identity Store

This feature is abstracted by the IdentityStore interface, and it's used to validate credentials and eventually retrieve group membership. In other words, it can provide capabilities for authentication, authorization or both.

IdentityStore is intended and encouraged to be used by the HttpAuthenticationMecanism through a called IdentityStoreHandler interface. A default implementation of the IdentityStoreHandler is provided by the Servletcontainer.

An application can provide its implementation of the IdentityStore or uses one of the two built-in implementations provided by the container for Database and LDAP.

4.1. Built-in Identity Stores

The Jakarta EE compliant server should provide implementations for the two Identity Stores: Database and LDAP.

The database IdentityStore implementation is initialized by passing a configuration data to the @DataBaseIdentityStoreDefinition annotation:

@DatabaseIdentityStoreDefinition( dataSourceLookup = "java:comp/env/jdbc/securityDS", callerQuery = "select password from users where username = ?", groupsQuery = "select GROUPNAME from groups where username = ?", priority=30) @ApplicationScoped public class AppConfig { }

As a configuration data, we need a JNDI data source to an external database, two JDBC statements for checking caller and his groups and finally a priority parameter which is used in case of multiples store are configured.

IdentityStore with high priority is processed later by the IdentityStoreHandler.

Like the database, LDAP IdentityStore implementation is initialized through the @LdapIdentityStoreDefinition by passing configuration data:

@LdapIdentityStoreDefinition( url = "ldap://localhost:10389", callerBaseDn = "ou=caller,dc=baeldung,dc=com", groupSearchBase = "ou=group,dc=baeldung,dc=com", groupSearchFilter = "(&(member=%s)(objectClass=groupOfNames))") @ApplicationScoped public class AppConfig { }

Here we need the URL of an external LDAP server, how to search the caller in the LDAP directory, and how to retrieve his groups.

4.2. Implementing a Custom IdentityStore

The IdentityStore interface defines four default methods:

default CredentialValidationResult validate( Credential credential) default Set getCallerGroups( CredentialValidationResult validationResult) default int priority() default Set validationTypes()

The priority() method returns a value for the order of iteration this implementation is processed by IdentityStoreHandler. An IdentityStore with lower priority is treated first.

By default, an IdentityStore processes both credentials validation (ValidationType.VALIDATE) and group retrieval(ValidationType.PROVIDE_GROUPS). We can override this behavior so that it can provide only one capability.

Thus, we can configure the IdentityStore to be used only for credentials validation:

@Override public Set validationTypes() { return EnumSet.of(ValidationType.VALIDATE); }

In this case, we should provide an implementation for the validate() method:

@ApplicationScoped public class InMemoryIdentityStore implements IdentityStore { // init from a file or harcoded private Map users = new HashMap(); @Override public int priority() { return 70; } @Override public Set validationTypes() { return EnumSet.of(ValidationType.VALIDATE); } public CredentialValidationResult validate( UsernamePasswordCredential credential) { UserDetails user = users.get(credential.getCaller()); if (credential.compareTo(user.getLogin(), user.getPassword())) { return new CredentialValidationResult(user.getLogin()); } return INVALID_RESULT; } }

Or we can choose to configure the IdentityStore so that it can be used only for group retrieval:

@Override public Set validationTypes() { return EnumSet.of(ValidationType.PROVIDE_GROUPS); }

We should then provide an implementation for the getCallerGroups() methods:

@ApplicationScoped public class InMemoryIdentityStore implements IdentityStore { // init from a file or harcoded private Map users = new HashMap(); @Override public int priority() { return 90; } @Override public Set validationTypes() { return EnumSet.of(ValidationType.PROVIDE_GROUPS); } @Override public Set getCallerGroups(CredentialValidationResult validationResult) { UserDetails user = users.get( validationResult.getCallerPrincipal().getName()); return new HashSet(user.getRoles()); } }

Because IdentityStoreHandler expects the implementation to be a CDI bean, we decorate it with ApplicationScoped annotation.

5. Security Context API

The Jakarta EE 8 Security API provides an access point to programmatic security through the SecurityContext interface. It's an alternative when the declarative security model enforced by the container isn't sufficient.

A default implementation of the SecurityContext interface should be provided at runtime as a CDI bean, and therefore we need to inject it:

@Inject SecurityContext securityContext;

At this point, we can authenticate the user, retrieve an authenticated one, check his role membership and grant or deny access to web resource through the five available methods.

5.1. Retrieving Caller Data

In previous versions of Jakarta EE, we'd retrieve the Principal or check the role membership differently in each container.

While we use the getUserPrincipal() and isUserInRole() methods of the HttpServletRequest in a servlet container, a similar methods getCallerPrincipal() and isCallerInRole() methods of the EJBContext are used in EJB Container.

The new Jakarta EE 8 Security API has standardized this by providing a similar method through the SecurityContext interface:

Principal getCallerPrincipal(); boolean isCallerInRole(String role);  Set getPrincipalsByType(Class type);

The getCallerPrincipal() method returns a container specific representation of the authenticated caller while the getPrincipalsByType() method retrieves all principals of a given type.

It can be useful in case the application specific caller is different from the container one.

5.2. Testing for Web Resource Access

First, we need to configure a protected resource:

@WebServlet("/protectedServlet") @ServletSecurity(@HttpConstraint(rolesAllowed = "USER_ROLE")) public class ProtectedServlet extends HttpServlet { //... }

And then, to check access to this protected resource we should invoke the hasAccessToWebResource() method:

securityContext.hasAccessToWebResource("/protectedServlet", "GET");

In this case, the method returns true if the user is in role USER_ROLE.

5.3. Authenticating the Caller Programmatically

An application can programmatically trigger the authentication process by invoking authenticate():

AuthenticationStatus authenticate( HttpServletRequest request, HttpServletResponse response, AuthenticationParameters parameters);

The container is then notified and will, in turn, invoke the authentication mechanism configured for the application. AuthenticationParameters parameter provides a credential to HttpAuthenticationMechanism:

withParams().credential(credential)

The SUCCESS and SEND_FAILURE values of the AuthenticationStatus design a successful and failed authentication while SEND_CONTINUE signals an in progress status of the authentication process.

6. Running the Examples

For highlighting these examples, we've used the latest development build of the Open Liberty Server which supports Jakarta EE 8. This is downloaded and installed thanks to the liberty-maven-plugin which can also deploy the application and start the server.

To run the examples, just access to the corresponding module and invoke this command:

mvn clean package liberty:run

As a result, Maven will download the server, build, deploy, and run the application.

7. Conclusion

Dans cet article, nous avons couvert la configuration et l'implémentation des principales fonctionnalités de la nouvelle API de sécurité Jakarta EE 8.

Tout d'abord, nous avons commencé par montrer comment configurer les mécanismes d'authentification intégrés par défaut et comment en implémenter un personnalisé. Plus tard, nous avons vu comment configurer le magasin d'identité intégré et comment en implémenter un personnalisé. Et enfin, nous avons vu comment appeler les méthodes du SecurityContext.

Comme toujours, les exemples de code de cet article sont disponibles à l'adresse over sur GitHub.