DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Jakarta Security and REST in the Cloud Part 1: Hello World
  • Jakarta Security and REST on Cloud: Part 4 Combining JWT With OAuth2
  • Jakarta Security and REST in the Cloud Part 3: Knowing the OAuth2
  • How to Develop Microservices With Spring Cloud and Netflix Discovery

Trending

  • Strategies for Securing E-Commerce Applications
  • When Airflow Tasks Get Stuck in Queued: A Real-World Debugging Story
  • Designing AI Multi-Agent Systems in Java
  • Data Lake vs. Warehouse vs. Lakehouse vs. Mart: Choosing the Right Architecture for Your Business
  1. DZone
  2. Data Engineering
  3. Databases
  4. Jakarta Security and REST in the Cloud: Part 2 Getting to Know the Basics

Jakarta Security and REST in the Cloud: Part 2 Getting to Know the Basics

Jakarta Security and REST in the Cloud: Part 2 Getting to Know the Basic

By 
Otavio Santana user avatar
Otavio Santana
DZone Core CORE ·
Jun. 14, 20 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
5.6K Views

Join the DZone community and get the full member experience.

Join For Free

Security is generally a topic that we always leave out when we talk about software architecture, but that does not mean that it is not essential. To talk more about the subject, we created this series on Java API security with Jakarta EE. In this second part, we will speak of the BASIC authentication process, how to implement it with a non-relational database, in this case, MongoDB, and how to quickly take it to the cloud.

BASIC is an authentication process where the user needs to enter their respective credentials, such as username and password, through each request in the header when they want to make a request to the server. In this case, the request header will have an Authorization: Basic <credential> where the credential is the username and password separated by “:” encoded using Base 64.

BASIC authentication workflow

The great advantage of this mechanism is found in the simplicity of the implementation. However, there are some problems:

  • In each request, it is necessary to pass the user's credentials and sensitive information. The less sensitive information is sent, the better.

  • Contrary to what many people imagine, Base 64 is not an encryption, and it is very easy to decode it to find the user's data from it.

  • Thinking of a distributed architecture with microservices, all requests will have to carry out a validation to authenticate and authorize the user, which may result in an overload in the security service.

  • Again, thinking about a distributed environment with microservices, if the first request needs more information from other services, you will always have to pass the critical information to the premises. This always enhances the chance of a man-in-the-middle attack and someone capturing that information. Hence, the importance of using secure communication.

Having explained some of the advantages and disadvantages of using BASIC as a mechanism, we will continue the article by creating integration with a database. In this case, we will use a NoSQL database, MongoDB. In a very simple way, we will use the previous example as a base, so we will create a new implementation of  IdentityStore. However, instead of leaving the information in memory, we will get it from the database Dice.

Based on our Hello World example using  the security API, we will add the mechanism to our dependencies to work with and integrate with the database. Since we are working with both Jakarta and NoSQL, our best option is undoubtedly Jakarta NoSQL.

XML
x
10
 
1
<dependency>
2
    <groupId>org.eclipse.jnosql.artemis</groupId>
3
    <artifactId>artemis-document</artifactId>
4
    <version>${jnosql.version}</version>
5
</dependency>
6
<dependency>
7
    <groupId>org.eclipse.jnosql.diana</groupId>
8
    <artifactId>mongodb-driver</artifactId>
9
    <version>${jnosql.version}</version>
10
</dependency>


Starting with the code, indeed, the first step is in modeling. For that reason, we will create our user with the minimum of necessary fields; in this case, the nickname, password, and roles that the user will have:

Java
xxxxxxxxxx
1
26
 
1
import jakarta.nosql.mapping.Column;
2
import jakarta.nosql.mapping.Entity;
3
import jakarta.nosql.mapping.Id;
4
 
          
5
import javax.security.enterprise.identitystore.Pbkdf2PasswordHash;
6
import java.util.Collections;
7
import java.util.HashSet;
8
import java.util.Set;
9
import java.util.stream.Collectors;
10
 
          
11
import static java.util.Objects.requireNonNull;
12
 
          
13
@Entity
14
public class User {
15
 
          
16
    @Id
17
    private String name;
18
 
          
19
    @Column
20
    private String password;
21
 
          
22
    @Column
23
    private Set<Role> roles;
24
    //...
25
}


A significant thing about the password is that we should never store it directly. The best option will always be to save the password hash and compare it. In terms of code design, a question arises:

How do we create an instance and update the user's password? Leaving the public attribute, we already know that it is not a way out. But what if we use the classic getter and setter? To answer this question, I will use another one: Is there any guarantee that whoever uses the setter will handle the hash before modifying it?

The answer is simple: no. This demonstrates that there are encapsulation problems, even using the private-public getter and setter attributes. To ensure that every time the user updates the password, it will not be saved without the hash, we will create an update method from which we will pass Pbkdf2PasswordHash, and we will leave all the work to this dependency. The goal is to demonstrate that a rich model helps to create a fail-safe API different from a simple model.

Java
xxxxxxxxxx
1
 
1
void updatePassword(String password, Pbkdf2PasswordHash passwordHash) {
2
  this.password = passwordHash.generate(password.toCharArray());
3
}


Another point is that we kept the method visible only to the package. It is vital to point out that the lower the visibility, the better. It is something that Java makes very clear about minimizing the accessibility of the fields, and it is worth following it.

With the update method completed, how will we do the insertion? This is simpler. We can use the Builder pattern, moving the creation logic, including the hash logic in the class. So we maintained the principle of SOLID's sole responsibility as well as avoiding the lack of encapsulation.

Java
xxxxxxxxxx
1
 
1
User user = User.builder()
2
    .withPasswordHash(passwordHash)
3
    .withPassword(password)
4
    .withName(name)
5
    .withRoles(roles)
6
    .build();
7
 
          


An important point to be emphasized is that it is useless to be careful with storing the password if this information, even with the hash, ends up leaking. Thus, we need to be careful when using notation to ignore the serialization of this information or to make explicit the information that will be transmitted by the service with a DTO layer.

In this integration, we also need to have a service to create and change users, and we will do this with the SecurityService class, as shown in the code below:

Java
xxxxxxxxxx
1
109
 
1
import javax.enterprise.context.ApplicationScoped;
2
import javax.inject.Inject;
3
import javax.security.enterprise.SecurityContext;
4
import javax.security.enterprise.identitystore.Pbkdf2PasswordHash;
5
import java.security.Principal;
6
import java.util.Collections;
7
import java.util.List;
8
import java.util.Set;
9
import java.util.stream.Collectors;
10
 
          
11
@ApplicationScoped
12
class SecurityService {
13
 
          
14
    @Inject
15
    private UserRepository repository;
16
 
          
17
    @Inject
18
    private Pbkdf2PasswordHash passwordHash;
19
 
          
20
    @Inject
21
    private SecurityContext securityContext;
22
 
          
23
    void create(UserDTO userDTO) {
24
        if (repository.existsById(userDTO.getName())) {
25
            throw new UserAlreadyExistException("There is an user with this id: " + userDTO.getName());
26
        } else {
27
            User user = User.builder()
28
                    .withPasswordHash(passwordHash)
29
                    .withPassword(userDTO.getPassword())
30
                    .withName(userDTO.getName())
31
                    .withRoles(getRole())
32
                    .build();
33
            repository.save(user);
34
        }
35
    }
36
 
          
37
    void delete(String id) {
38
        repository.deleteById(id);
39
    }
40
 
          
41
    void updatePassword(String id, UserDTO dto) {
42
 
          
43
        final Principal principal = securityContext.getCallerPrincipal();
44
        if (isForbidden(id, securityContext, principal)) {
45
            throw new UserForbiddenException();
46
        }
47
 
          
48
        final User user = repository.findById(id)
49
                .orElseThrow(() -> new UserNotFoundException(id));
50
        user.updatePassword(dto.getPassword(), passwordHash);
51
        repository.save(user);
52
    }
53
 
          
54
 
          
55
    public void addRole(String id, RoleDTO dto) {
56
        final User user = repository.findById(id)
57
                .orElseThrow(() -> new UserNotFoundException(id));
58
 
          
59
        user.addRoles(dto.getRoles());
60
        repository.save(user);
61
 
          
62
    }
63
 
          
64
    public void removeRole(String id, RoleDTO dto) {
65
        final User user = repository.findById(id)
66
                .orElseThrow(() -> new UserNotFoundException(id));
67
        user.removeRoles(dto.getRoles());
68
        repository.save(user);
69
    }
70
 
          
71
    public UserDTO getUser() {
72
        final Principal principal = securityContext.getCallerPrincipal();
73
        if (principal == null) {
74
            throw new UserNotAuthorizedException();
75
        }
76
        final User user = repository.findById(principal.getName())
77
                .orElseThrow(() -> new UserNotFoundException(principal.getName()));
78
        UserDTO dto = toDTO(user);
79
        return dto;
80
    }
81
 
          
82
    public List<UserDTO> getUsers() {
83
        return repository.findAll()
84
                .map(this::toDTO)
85
                .collect(Collectors.toList());
86
    }
87
 
          
88
    private UserDTO toDTO(User user) {
89
        UserDTO dto = new UserDTO();
90
        dto.setName(user.getName());
91
        dto.setRoles(user.getRoles());
92
        return dto;
93
    }
94
 
          
95
    private Set<Role> getRole() {
96
        if (repository.count() == 0) {
97
            return Collections.singleton(Role.ADMIN);
98
        } else {
99
            return Collections.singleton(Role.USER);
100
        }
101
    }
102
 
          
103
    private boolean isForbidden(String id, SecurityContext context, Principal principal) {
104
        return !(context.isCallerInRole(Role.ADMIN.name()) || id.equals(principal.getName()));
105
    }
106
 
          
107
 
          
108
}
109
 
          


There are two important points to note in the SecurityService class. The first is in the SecurityContext interface, which represents the user's information if logged in, thanks to Jakarta Security. The other point is in the exceptions, as they are all common runtime exceptions, however they are followed by a JAX-RS exception mapper. This approach aims not to “leak” the rules of the controller, which in this case, JAX-RS, is within the business rule. Thus, we launched a business exception that will then be translated into a JAX-RS exception. This approach tends to facilitate testing by avoiding layer leak or layer leakage. This is a very famous good practice, both for DDD, Clean Architecture, and for hexagonal architecture.

Java
xxxxxxxxxx
1
23
 
1
public class UserForbiddenException extends RuntimeException {
2
}
3
 
          
4
public class UserNotAuthorizedException extends RuntimeException {
5
}
6
 
          
7
@Provider
8
public class UserForbiddenExceptionMapper implements ExceptionMapper<UserForbiddenException> {
9
    @Override
10
    public Response toResponse(UserForbiddenException exception) {
11
        return Response.status(Response.Status.FORBIDDEN).build();
12
    }
13
}
14
 
          
15
@Provider
16
public class UserNotAuthorizedExceptionMapper implements ExceptionMapper<UserNotAuthorizedException> {
17
 
          
18
    @Override
19
    public Response toResponse(UserForbiddenException exception) {
20
        return Response.status(Response.Status.UNAUTHORIZED).build();
21
    }
22
}
23
 
          


We will have SecurityResource in the resource that will be responsible for creating and managing users, and the class will have some operations that only a user profile can perform. In our example, we chose to use the DTO when performing the conversion manually, due to the fact that we have few fields, however, if there are more fields, the use of a mapper is always recommended.

Java
xxxxxxxxxx
1
69
 
1
import javax.annotation.security.RolesAllowed;
2
import javax.inject.Inject;
3
import javax.validation.Valid;
4
import javax.ws.rs.Consumes;
5
import javax.ws.rs.DELETE;
6
import javax.ws.rs.GET;
7
import javax.ws.rs.POST;
8
import javax.ws.rs.PUT;
9
import javax.ws.rs.Path;
10
import javax.ws.rs.PathParam;
11
import javax.ws.rs.Produces;
12
import javax.ws.rs.core.MediaType;
13
import java.util.List;
14
 
          
15
@Path("security")
16
@Consumes(MediaType.APPLICATION_JSON)
17
@Produces(MediaType.APPLICATION_JSON)
18
public class SecurityResource {
19
 
          
20
    @Inject
21
    private SecurityService service;
22
 
          
23
    @POST
24
    public void create(@Valid UserDTO userDTO) {
25
        service.create(userDTO);
26
    }
27
 
          
28
    @DELETE
29
    @Path("{id}")
30
    @RolesAllowed("ADMIN")
31
    public void delete(@PathParam("id") String id) {
32
        service.delete(id);
33
    }
34
 
          
35
 
          
36
    @Path("{id}")
37
    @PUT
38
    public void changePassword(@PathParam("id") String id, @Valid UserDTO dto) {
39
        service.updatePassword(id, dto);
40
    }
41
 
          
42
    @Path("roles/{id}")
43
    @PUT
44
    @RolesAllowed("ADMIN")
45
    public void addRole(@PathParam("id") String id, RoleDTO dto){
46
        service.addRole(id, dto);
47
    }
48
 
          
49
    @Path("roles/{id}")
50
    @DELETE
51
    @RolesAllowed("ADMIN")
52
    public void removeRole(@PathParam("id") String id, RoleDTO dto){
53
        service.removeRole(id, dto);
54
    }
55
 
          
56
    @Path("me")
57
    @GET
58
    public UserDTO getMe() {
59
        return service.getUser();
60
    }
61
 
          
62
    @Path("users")
63
    @GET
64
    @RolesAllowed("ADMIN")
65
    public List<UserDTO> getUsers() {
66
        return service.getUsers();
67
    }
68
}
69
 
          


A feature that saves a lot and validates the input data is certainly Bean Validation. However, it is crucial to ensure that such validation messages are transmitted and not just the error code. For example, the information that the password is mandatory and that it must be at least six characters long. One way to send the error information to the user is to use the JAX-RS exception Mapper again.

Java
xxxxxxxxxx
1
26
 
1
import javax.validation.ConstraintViolation;
2
import javax.validation.ConstraintViolationException;
3
import javax.ws.rs.core.MediaType;
4
import javax.ws.rs.core.Response;
5
import javax.ws.rs.ext.ExceptionMapper;
6
import javax.ws.rs.ext.Provider;
7
import java.util.List;
8
import java.util.Set;
9
import java.util.stream.Collectors;
10
 
          
11
@Provider
12
public class BeanValConstrainViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
13
 
          
14
    @Override
15
    public Response toResponse(ConstraintViolationException e) {
16
        Set<ConstraintViolation<?>> cv = e.getConstraintViolations();
17
        final List<String> errors = cv.stream().map(c -> c.getPropertyPath() + " " + c.getMessage()).collect(Collectors.toList());
18
        return Response.status(Response.Status.BAD_REQUEST)
19
                .entity(new ErrorMessage(errors))
20
                .type(MediaType.APPLICATION_JSON)
21
                .build();
22
    }
23
 
          
24
 
          
25
}


The last step of our code is the creation of IdentityStore, where we will search for the user's information. As mentioned, we make the query based on the user and the hash that was generated from the password.

Java
xxxxxxxxxx
1
48
 
1
import javax.enterprise.context.ApplicationScoped;
2
import javax.inject.Inject;
3
import javax.security.enterprise.credential.Credential;
4
import javax.security.enterprise.credential.Password;
5
import javax.security.enterprise.credential.UsernamePasswordCredential;
6
import javax.security.enterprise.identitystore.CredentialValidationResult;
7
import javax.security.enterprise.identitystore.IdentityStore;
8
import javax.security.enterprise.identitystore.Pbkdf2PasswordHash;
9
import java.util.Optional;
10
 
          
11
import static javax.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT;
12
 
          
13
@ApplicationScoped
14
public class MongoDBIdentity implements IdentityStore {
15
 
          
16
    @Inject
17
    private UserRepository repository;
18
 
          
19
    @Inject
20
    private Pbkdf2PasswordHash passwordHash;
21
 
          
22
    @Override
23
    public int priority() {
24
        return 10;
25
    }
26
 
          
27
    @Override
28
    public CredentialValidationResult validate(Credential credential) {
29
 
          
30
        if (credential instanceof UsernamePasswordCredential) {
31
            UsernamePasswordCredential userCredential = UsernamePasswordCredential
32
                    .class.cast(credential);
33
 
          
34
            final Password userPassword = userCredential.getPassword();
35
            final Optional<User> userOptional = repository.findById(userCredential.getCaller());
36
            if (userOptional.isPresent()) {
37
                final User user = userOptional.get();
38
                if (passwordHash.verify(userPassword.getValue(), user.getPassword())) {
39
                    return new CredentialValidationResult(user.getName(), user.getRoles());
40
                }
41
            }
42
 
          
43
        }
44
        return INVALID_RESULT;
45
    }
46
 
          
47
}


The code is now ready, requiring only local testing. A very easy and intuitive option is certainly Docker.

With that done, we can already perform some tests of our application.

Shell
xxxxxxxxxx
1
 
1
curl --location --request POST 'http://localhost:8080/security' --header 'Content-Type: application/json' --data-raw '{"name": "otavio", "password": "otavio"}'
2
 
          
3
curl --location --request GET 'http://localhost:8080/admin' //returns 401
4
 
          
5
curl --location --request GET 'http://localhost:8080/admin' --header 'Authorization: Basic b3RhdmlvOm90YXZpbw==' //returns 200


Moving to the Cloud

We already mentioned that we will use a PaaS to facilitate the deployment in the cloud of our application. In addition to the principles, feel free to review the first part of this series. Based on the Hello World example, we will mention the changes. As MongoDB will be used, the service file will be modified to add MongoDB to be managed by the platform.

YAML
xxxxxxxxxx
1
 
1
mongodb:
2
  type: mongodb:3.6
3
  disk: 1024


The application configuration file will have two modifications. The first is related to the relationship that this application will have with the MongoDB database and the other is to overwrite the access information to the bank so that our application is not aware of these credentials. A good practice that we already mentioned, comes from The Twelve Factor App. After making the necessary changes, just push that Platform.sh will create the instance and manage everything, so that we can focus on the business.

YAML
xxxxxxxxxx
1
25
 
1
name: app
2
type: "java:11"
3
disk: 1024
4
hooks:
5
    build:  mvn clean package payara-micro:bundle
6
7
relationships:
8
    mongodb: 'mongodb:mongodb'
9
10
web:
11
    commands:
12
        start: |
13
            export MONGO_PORT=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].port"`
14
            export MONGO_HOST=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].host"`
15
            export MONGO_ADDRESS="${MONGO_HOST}:${MONGO_PORT}"
16
            export MONGO_PASSWORD=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].password"`
17
            export MONGO_USER=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].username"`
18
            export MONGO_DATABASE=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].path"`
19
            java -jar -Xmx$(jq .info.limits.memory /run/config.json)m -XX:+ExitOnOutOfMemoryError \
20
            -Ddocument.settings.jakarta.nosql.host=$MONGO_ADDRESS \
21
            -Ddocument.database=$MONGO_DATABASE -Ddocument.settings.jakarta.nosql.user=$MONGO_USER \
22
            -Ddocument.settings.jakarta.nosql.password=$MONGO_PASSWORD \
23
            -Ddocument.settings.mongodb.authentication.source=$MONGO_DATABASE \
24
            target/microprofile-microbundle.jar --port $PORT


With that, we talked a little about BASIC and its advantages and disadvantages using Jakarta Security and MongoDB as a database. We mentioned how encapsulation is also related to security issues and how it impacts the software. See you in the next part of the series where we’ll talk about Auth. You can certainly have access to all the code for the second part here.

security REST Web Protocols Cloud Java (programming language) Database application microservice Man-in-the-middle attack

Opinions expressed by DZone contributors are their own.

Related

  • Jakarta Security and REST in the Cloud Part 1: Hello World
  • Jakarta Security and REST on Cloud: Part 4 Combining JWT With OAuth2
  • Jakarta Security and REST in the Cloud Part 3: Knowing the OAuth2
  • How to Develop Microservices With Spring Cloud and Netflix Discovery

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • [email protected]

Let's be friends: