{{announcement.body}}
{{announcement.title}}

Jakarta Security and REST in the Cloud Part 3: Knowing the OAuth2

DZone 's Guide to

Jakarta Security and REST in the Cloud Part 3: Knowing the OAuth2

Jakarta Security and REST in the Cloud: Part 2 Getting to Know the OAuth 2.0.

· Java Zone ·
Free Resource

Security is generally a topic that is left out when talking about software architecture, but that doesn’t mean that it is not essential. To talk more about it, we created this series on Java API security with Jakarta EE. In this third part, we will talk about the OAuth2 authentication process, moving it quickly to the cloud, and how to implement it with two MongoDB and Redis databases.

OAuth 2.0 is a protocol that allows permission to access authorization resources between systems or sites with the benefits of a better encapsulation of critical information such as username and password. An overview of OAuth 2.0:

  • The first step is the authorization request. It identifies the user's authenticity
  • Once authorized, the next step is to request the token
  • With this access token, all requests will be made from it

This is a very short summary, but if you want to know more about OAuth 2.0, it is worth taking a look at the specification.

One of the significant advantages of this approach is low password exposure, in addition to being able to generate a large number of tokens for the same user. It is possible to create a token for each device, and if you wanted to revoke access to a specific device, you would just remove the token from this device. Another critical point is that none of these devices have access to the user's login and password, only the token. Thinking about encapsulation, the less exposed the password, the better for security.

On the other hand, we have significantly increased the complexity of the architecture; after all, before it was just a password, now, we have a large volume of tokens managed by each user.

OAuth2 in architectural terms will follow the following steps:

  1. The first step, following the rule, is to request to obtain a new token
  2. This request will result in a pair of tokens: an access_token and a refresh_token
  3. From that moment, every request will be made with the "access_token"
  4. The access_token will expire at some point making it useless
  5. The next step will be to update it, creating a new token, thanks to refresh_token. We will make a new request, however, this time with the refresh_token that will return the result already mentioned in step 2
  6. The cycle will continue again using access_token until it expires back

This cycle only applies to one device -- if it is necessary to have another access to another machine, each one will have its period with its respective token.

It is possible to separate the logic that generates tokens from the user's information logically and physically, that is, on another server. But in our example, we will simplify by demonstrating a logical, but not physical, separation. The authentication mechanism logic will be created as a subdomain of the security API since there is a dependency to authenticate the user. Since they have a TTL, we will use Redis.


When we talk about the code, in general, we will take advantage of practically all the logic of storage and user management from the second part of the article, with the difference that the mechanism will be different. We will add some others that deal with the generation of tokens with Redis.

XML
 




x
11


1
<dependency>
2
    <groupId>org.eclipse.jnosql.artemis</groupId>
3
    <artifactId>artemis-key-value</artifactId>
4
    <version>${jnosql.version}</version>
5
</dependency>
6
<dependency>
7
    <groupId>org.eclipse.jnosql.diana</groupId>
8
    <artifactId>redis-driver</artifactId>
9
    <version>${jnosql.version}</version>
10
</dependency>
11
 
          



Regarding the modeling and persistence of the tokens, three new entities will be created. One important thing is that the encapsulation rules are still relevant. And it is only for this reason that we are using private visibility and public access methods, in cases where we have no other option. It is a good rule that Effective Java speaks so much about, besides being a good practice for security. The critical point is that these entities, in addition to Jakarta NoSQL notations, also use JSONB notations. The reason for this is that Redis stores the information as text, and the storage strategy that the Redis driver uses is in JSON, using some kind of implementation from the Jakarta world.

Java
 




xxxxxxxxxx
1
47


1
@Entity
2
@JsonbVisibility(FieldPropertyVisibilityStrategy.class)
3
public class UserToken {
4
 
          
5
    @Id
6
    @JsonbProperty
7
    private String username;
8
 
          
9
    @Column
10
    @JsonbProperty
11
    private Set<Token> tokens;
12
 //...
13
}
14
 
          
15
@Entity
16
@JsonbVisibility(FieldPropertyVisibilityStrategy.class)
17
public class RefreshToken {
18
 
          
19
    @Id
20
    @JsonbProperty
21
    private String id;
22
 
          
23
    @JsonbProperty
24
    private String token;
25
 
          
26
    @JsonbProperty
27
    private String accessToken;
28
    @JsonbProperty
29
    private String user;
30
 
          
31
    //...
32
}
33
 
          
34
@Entity
35
@JsonbVisibility(FieldPropertyVisibilityStrategy.class)
36
public class AccessToken {
37
 
          
38
    @Id
39
    private String id;
40
    @JsonbProperty
41
    private String user;
42
    @JsonbProperty
43
    private String token;
44
 
          
45
    //...
46
}
47
 
          



Going to the rule for generating and updating these tokens, we have the class OAuth2Service that will manage all the logic of the storage mechanism. Data validation will be performed from Bean Validation, and, as it starts from the context, both for creating and updating the token, we create a group for each setting. Thus, as in MongoDB, the value-key API can use the Repository; however, as the operations are quite simple, the KeyValueTemplate will be used. An important point is when the refresh token is persisted, having a second parameter that defines the TTL. This means that, after a specific time, the information will be automatically deleted from Redis.

Java
 




xxxxxxxxxx
1
147


1
import jakarta.nosql.mapping.keyvalue.KeyValueTemplate;
2
import org.eclipse.microprofile.config.inject.ConfigProperty;
3
import sh.platform.sample.security.SecurityService;
4
import sh.platform.sample.security.User;
5
import sh.platform.sample.security.UserNotAuthorizedException;
6
 
          
7
import javax.enterprise.context.ApplicationScoped;
8
import javax.inject.Inject;
9
import javax.validation.ConstraintViolation;
10
import javax.validation.ConstraintViolationException;
11
import javax.validation.Validator;
12
import java.time.Duration;
13
import java.util.Arrays;
14
import java.util.Set;
15
 
          
16
@ApplicationScoped
17
class Oauth2Service {
18
 
          
19
    static final int EXPIRE_IN = 3600;
20
 
          
21
    static final Duration EXPIRES = Duration.ofSeconds(EXPIRE_IN);
22
 
          
23
    @Inject
24
    private SecurityService securityService;
25
 
          
26
    @Inject
27
    @ConfigProperty(name = "keyvalue")
28
    private KeyValueTemplate template;
29
 
          
30
    @Inject
31
    private Validator validator;
32
 
          
33
    public Oauth2Response token(Oauth2Request request) {
34
 
          
35
        final Set<ConstraintViolation<Oauth2Request>> violations = validator.validate(request, Oauth2Request
36
                .GenerateToken.class);
37
        if (!violations.isEmpty()) {
38
            throw new ConstraintViolationException(violations);
39
        }
40
 
          
41
        final User user = securityService.findBy(request.getUsername(), request.getPassword());
42
        final UserToken userToken = template.get(request.getUsername(), UserToken.class)
43
                .orElse(new UserToken(user.getName()));
44
 
          
45
        final Token token = Token.generate();
46
 
          
47
        AccessToken accessToken = new AccessToken(token, user.getName());
48
        RefreshToken refreshToken = new RefreshToken(userToken, token, user.getName());
49
 
          
50
        template.put(refreshToken, EXPIRES);
51
        template.put(Arrays.asList(userToken, accessToken));
52
 
          
53
        final Oauth2Response response = Oauth2Response.of(accessToken, refreshToken, EXPIRE_IN);
54
        return response;
55
    }
56
 
          
57
    public Oauth2Response refreshToken(Oauth2Request request) {
58
        final Set<ConstraintViolation<Oauth2Request>> violations = validator.validate(request, Oauth2Request
59
                .RefreshToken.class);
60
        if (!violations.isEmpty()) {
61
            throw new ConstraintViolationException(violations);
62
        }
63
 
          
64
        RefreshToken refreshToken = template.get(RefreshToken.PREFIX + request.getRefreshToken(), RefreshToken.class)
65
                .orElseThrow(() -> new UserNotAuthorizedException("Invalid Token"));
66
 
          
67
        final UserToken userToken = template.get(refreshToken.getUser(), UserToken.class)
68
                .orElse(new UserToken(refreshToken.getUser()));
69
 
          
70
        final Token token = Token.generate();
71
        AccessToken accessToken = new AccessToken(token, refreshToken.getUser());
72
        refreshToken.update(accessToken, userToken, template);
73
        template.put(accessToken, EXPIRES);
74
        final Oauth2Response response = Oauth2Response.of(accessToken, refreshToken, EXPIRE_IN);
75
        return response;
76
    }
77
 
          
78
}
79
 
          
80
import sh.platform.sample.security.infra.FieldPropertyVisibilityStrategy;
81
 
          
82
import javax.json.bind.annotation.JsonbVisibility;
83
import javax.validation.constraints.NotBlank;
84
import javax.ws.rs.FormParam;
85
 
          
86
@JsonbVisibility(FieldPropertyVisibilityStrategy.class)
87
public class Oauth2Request {
88
 
          
89
    @FormParam("grand_type")
90
    @NotBlank
91
    private String grandType;
92
 
          
93
    @FormParam("username")
94
    @NotBlank(groups = {GenerateToken.class})
95
    private String username;
96
 
          
97
    @FormParam("password")
98
    @NotBlank(groups = {GenerateToken.class})
99
    private String password;
100
 
          
101
    @FormParam("refresh_token")
102
    @NotBlank(groups = {RefreshToken.class})
103
    private String refreshToken;
104
 
          
105
    public void setGrandType(GrantType grandType) {
106
        if(grandType != null) {
107
            this.grandType = grandType.get();
108
        }
109
    }
110
 
          
111
    public void setUsername(String username) {
112
        this.username = username;
113
    }
114
 
          
115
    public void setPassword(String password) {
116
        this.password = password;
117
    }
118
 
          
119
    public void setRefreshToken(String refreshToken) {
120
        this.refreshToken = refreshToken;
121
    }
122
 
          
123
    public GrantType getGrandType() {
124
        if(grandType != null) {
125
            return GrantType.parse(grandType);
126
        }
127
        return null;
128
    }
129
 
          
130
    public String getUsername() {
131
        return username;
132
    }
133
 
          
134
    public String getPassword() {
135
        return password;
136
    }
137
 
          
138
    public String getRefreshToken() {
139
        return refreshToken;
140
    }
141
 
          
142
 
          
143
    public @interface  GenerateToken{}
144
 
          
145
    public @interface  RefreshToken{}
146
}
147
 
          



Within the OAuth2 feature, you can see that we have a method and two operations. It is one of the reasons that in the code, the getGrantType method returns an enum with two options.

Java
 




xxxxxxxxxx
1
34


 
1
import javax.enterprise.context.ApplicationScoped;
2
import javax.inject.Inject;
3
import javax.validation.Valid;
4
import javax.ws.rs.BeanParam;
5
import javax.ws.rs.Consumes;
6
import javax.ws.rs.POST;
7
import javax.ws.rs.Path;
8
import javax.ws.rs.Produces;
9
import javax.ws.rs.core.MediaType;
10
 
          
11
@ApplicationScoped
12
@Path("oauth2")
13
public class Oauth2Resource {
14
 
          
15
 
          
16
    @Inject
17
    private Oauth2Service service;
18
 
          
19
    @POST
20
    @Path("token")
21
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
22
    @Produces(MediaType.APPLICATION_JSON)
23
    public Oauth2Response token(@BeanParam @Valid Oauth2Request request) {
24
        switch (request.getGrandType()) {
25
            case PASSWORD:
26
                return service.token(request);
27
            case REFRESH_TOKEN:
28
                return service.refreshToken(request);
29
            default:
30
                throw new UnsupportedOperationException("There is not support to another type");
31
        }
32
    }
33
}
34
 
          



The OAuth2Authentication class receives the request and searches for the “Authorization” header validating using the regex. Once the Header has been approved, the next step is to check the existence of this token within the database, in which case we use Redis. Once the refresh token is verified, we send the user ID to the storage mechanism represented by the IdentityStoreHandler.

The identification class needed some changes compared to the second part. It continues to load user information and permission rules, however, there is no password validation, as all of this is done by the user's identifier.

Java
 




xxxxxxxxxx
1
59


1
import jakarta.nosql.mapping.keyvalue.KeyValueTemplate;
2
import org.eclipse.microprofile.config.inject.ConfigProperty;
3
 
          
4
import javax.enterprise.context.ApplicationScoped;
5
import javax.inject.Inject;
6
import javax.security.enterprise.AuthenticationStatus;
7
import javax.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism;
8
import javax.security.enterprise.authentication.mechanism.http.HttpMessageContext;
9
import javax.security.enterprise.credential.CallerOnlyCredential;
10
import javax.security.enterprise.identitystore.CredentialValidationResult;
11
import javax.security.enterprise.identitystore.IdentityStoreHandler;
12
import javax.servlet.http.HttpServletRequest;
13
import javax.servlet.http.HttpServletResponse;
14
import java.util.Optional;
15
import java.util.regex.Matcher;
16
import java.util.regex.Pattern;
17
 
          
18
import static javax.security.enterprise.identitystore.CredentialValidationResult.Status.VALID;
19
 
          
20
@ApplicationScoped
21
public class Oauth2Authentication implements HttpAuthenticationMechanism {
22
 
          
23
    private static final Pattern CHALLENGE_PATTERN
24
            = Pattern.compile("^Bearer *([^ ]+) *$", Pattern.CASE_INSENSITIVE);
25
 
          
26
    @Inject
27
    private IdentityStoreHandler identityStoreHandler;
28
 
          
29
    @Inject
30
    @ConfigProperty(name = "keyvalue")
31
    private KeyValueTemplate template;
32
 
          
33
    @Override
34
    public AuthenticationStatus validateRequest(HttpServletRequest request, HttpServletResponse response,
35
                                                HttpMessageContext httpMessageContext) {
36
 
          
37
 
          
38
        final String authorization = request.getHeader("Authorization");
39
 
          
40
        Matcher matcher = CHALLENGE_PATTERN.matcher(Optional.ofNullable(authorization).orElse(""));
41
        if (!matcher.matches()) {
42
            return httpMessageContext.doNothing();
43
        }
44
        final String token = matcher.group(1);
45
        final Optional<AccessToken> optional = template.get(AccessToken.PREFIX + token, AccessToken.class);
46
 
          
47
        if (!optional.isPresent()) {
48
            return httpMessageContext.responseUnauthorized();
49
        }
50
        final AccessToken accessToken = optional.get();
51
        final CredentialValidationResult validate = identityStoreHandler.validate(new CallerOnlyCredential(accessToken.getUser()));
52
        if (validate.getStatus() == VALID) {
53
            return httpMessageContext.notifyContainerAboutLogin(validate.getCallerPrincipal(), validate.getCallerGroups());
54
        } else {
55
            return httpMessageContext.responseUnauthorized();
56
        }
57
    }
58
}
59
 
          



The critical point is that although the authentication mechanism depends on the database, they are not known, thanks to the security API. Following the domain and subdomain rule, the OAuth2 subdomain is allowed to see the security API. However, the opposite is not allowed to occur for several reasons. This cyclical dependency brings several problems, for example, if at some point we want to move the logic to a server. it will be more difficult because there is difficulty to maintain the software in this way. A simple way of thinking about cyclical addiction is to reflect on the classic chicken and egg question.

However, here's a problem: When a user has removed, it is also essential that the respective tokens will also be removed. One way to create this separation is through events. As we are using CDI, it will fire events.

Java
 




xxxxxxxxxx
1
33


1
import jakarta.nosql.mapping.keyvalue.KeyValueTemplate;
2
import org.eclipse.microprofile.config.inject.ConfigProperty;
3
import sh.platform.sample.security.RemoveToken;
4
import sh.platform.sample.security.RemoveUser;
5
import sh.platform.sample.security.User;
6
import sh.platform.sample.security.UserForbiddenException;
7
 
          
8
import javax.enterprise.context.ApplicationScoped;
9
import javax.enterprise.event.Observes;
10
import javax.inject.Inject;
11
import java.util.Collections;
12
import java.util.Optional;
13
import java.util.Set;
14
 
          
15
@ApplicationScoped
16
class Oauth2Observes {
17
 
          
18
 
          
19
    @Inject
20
    @ConfigProperty(name = "keyvalue")
21
    private KeyValueTemplate template;
22
 
          
23
    public void observe(@Observes RemoveUser removeUser) {
24
 
          
25
     //....
26
    }
27
 
          
28
    public void observe(@Observes RemoveToken removeToken) {
29
     //....
30
 
          
31
    }
32
}
33
 
          



Moving to the Cloud

In our series of articles, we are using a PaaS to simplify the complexity of hardware and security of access between containers. After all, it would be all in vain if we did strict control on the software, and the database had a public IP for the entire internet. Thus, we will maintain the strategy of using Platform.sh.

We will only mention changing the services file to add another database, in this case, Redis:

YAML
 




xxxxxxxxxx
1


 
1
mongodb:
2
  type: mongodb:3.6
3
  disk: 1024
4
 
          
5
redis:
6
  type: redis-persistent:5.0
7
  disk: 1024
8
 
          



It will be necessary to carry out the modification within the application configuration file. The goal is to provide credentials so that the application container has access to the database container.

YAML
 




xxxxxxxxxx
1
28


 
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
    redis: 'redis:redis'
10
 
          
11
web:
12
    commands:
13
        start: |
14
            export MONGO_PORT=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].port"`
15
            export MONGO_HOST=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].host"`
16
            export MONGO_ADDRESS="${MONGO_HOST}:${MONGO_PORT}"
17
            export MONGO_PASSWORD=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].password"`
18
            export MONGO_USER=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].username"`
19
            export MONGO_DATABASE=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].path"`
20
            export REDIS_HOST=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".redis[0].host"`
21
            java -jar -Xmx$(jq .info.limits.memory /run/config.json)m -XX:+ExitOnOutOfMemoryError \
22
            -Ddocument.settings.jakarta.nosql.host=$MONGO_ADDRESS \
23
            -Ddocument.database=$MONGO_DATABASE -Ddocument.settings.jakarta.nosql.user=$MONGO_USER \
24
            -Ddocument.settings.jakarta.nosql.password=$MONGO_PASSWORD \
25
            -Ddocument.settings.mongodb.authentication.source=$MONGO_DATABASE \
26
            -Dkeyvalue.settings.jakarta.nosql.host=$REDIS_HOST \
27
            target/microprofile-microbundle.jar --port $PORT
28
 
          



With that, we talk about the concepts and the design of an OAuth2 mechanism in a practical way, using Jakarta Security. The critical point is that for each request, we need to perform a search within the database, since the token is nothing more than a pointer to the information; however, it is not the information itself. There is an exciting way to use the symbol to put information, for example, by combining the token with JWT. This combination of OAuth2 and JWT will be scenes from the next chapter.

As always, the code example can be found on GitHub.

Topics:
cloud, jakarta, jakarta ee, java, oauth, paas, platform.sh, tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}