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.
Join the DZone community and get the full member experience.
Join For FreeSecurity 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:
- The first step, following the rule, is to request to obtain a new token
- This request will result in a pair of tokens: an access_token and a refresh_token
- From that moment, every request will be made with the "access_token"
- The access_token will expire at some point making it useless
- 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
- 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.
<dependency>
<groupId>org.eclipse.jnosql.artemis</groupId>
<artifactId>artemis-key-value</artifactId>
<version>${jnosql.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jnosql.diana</groupId>
<artifactId>redis-driver</artifactId>
<version>${jnosql.version}</version>
</dependency>
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.
xxxxxxxxxx
FieldPropertyVisibilityStrategy.class) (
public class UserToken {
private String username;
private Set<Token> tokens;
//...
}
FieldPropertyVisibilityStrategy.class) (
public class RefreshToken {
private String id;
private String token;
private String accessToken;
private String user;
//...
}
FieldPropertyVisibilityStrategy.class) (
public class AccessToken {
private String id;
private String user;
private String token;
//...
}
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.
xxxxxxxxxx
import jakarta.nosql.mapping.keyvalue.KeyValueTemplate;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import sh.platform.sample.security.SecurityService;
import sh.platform.sample.security.User;
import sh.platform.sample.security.UserNotAuthorizedException;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import java.time.Duration;
import java.util.Arrays;
import java.util.Set;
class Oauth2Service {
static final int EXPIRE_IN = 3600;
static final Duration EXPIRES = Duration.ofSeconds(EXPIRE_IN);
private SecurityService securityService;
name = "keyvalue") (
private KeyValueTemplate template;
private Validator validator;
public Oauth2Response token(Oauth2Request request) {
final Set<ConstraintViolation<Oauth2Request>> violations = validator.validate(request, Oauth2Request
.GenerateToken.class);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
final User user = securityService.findBy(request.getUsername(), request.getPassword());
final UserToken userToken = template.get(request.getUsername(), UserToken.class)
.orElse(new UserToken(user.getName()));
final Token token = Token.generate();
AccessToken accessToken = new AccessToken(token, user.getName());
RefreshToken refreshToken = new RefreshToken(userToken, token, user.getName());
template.put(refreshToken, EXPIRES);
template.put(Arrays.asList(userToken, accessToken));
final Oauth2Response response = Oauth2Response.of(accessToken, refreshToken, EXPIRE_IN);
return response;
}
public Oauth2Response refreshToken(Oauth2Request request) {
final Set<ConstraintViolation<Oauth2Request>> violations = validator.validate(request, Oauth2Request
.RefreshToken.class);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
RefreshToken refreshToken = template.get(RefreshToken.PREFIX + request.getRefreshToken(), RefreshToken.class)
.orElseThrow(() -> new UserNotAuthorizedException("Invalid Token"));
final UserToken userToken = template.get(refreshToken.getUser(), UserToken.class)
.orElse(new UserToken(refreshToken.getUser()));
final Token token = Token.generate();
AccessToken accessToken = new AccessToken(token, refreshToken.getUser());
refreshToken.update(accessToken, userToken, template);
template.put(accessToken, EXPIRES);
final Oauth2Response response = Oauth2Response.of(accessToken, refreshToken, EXPIRE_IN);
return response;
}
}
import sh.platform.sample.security.infra.FieldPropertyVisibilityStrategy;
import javax.json.bind.annotation.JsonbVisibility;
import javax.validation.constraints.NotBlank;
import javax.ws.rs.FormParam;
FieldPropertyVisibilityStrategy.class) (
public class Oauth2Request {
"grand_type") (
private String grandType;
"username") (
groups = {GenerateToken.class}) (
private String username;
"password") (
groups = {GenerateToken.class}) (
private String password;
"refresh_token") (
groups = {RefreshToken.class}) (
private String refreshToken;
public void setGrandType(GrantType grandType) {
if(grandType != null) {
this.grandType = grandType.get();
}
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public GrantType getGrandType() {
if(grandType != null) {
return GrantType.parse(grandType);
}
return null;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getRefreshToken() {
return refreshToken;
}
public @interface GenerateToken{}
public @interface RefreshToken{}
}
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.
xxxxxxxxxx
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.BeanParam;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
"oauth2") (
public class Oauth2Resource {
private Oauth2Service service;
"token") (
MediaType.APPLICATION_FORM_URLENCODED) (
MediaType.APPLICATION_JSON) (
public Oauth2Response token( Oauth2Request request) {
switch (request.getGrandType()) {
case PASSWORD:
return service.token(request);
case REFRESH_TOKEN:
return service.refreshToken(request);
default:
throw new UnsupportedOperationException("There is not support to another type");
}
}
}
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.
xxxxxxxxxx
import jakarta.nosql.mapping.keyvalue.KeyValueTemplate;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.security.enterprise.AuthenticationStatus;
import javax.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism;
import javax.security.enterprise.authentication.mechanism.http.HttpMessageContext;
import javax.security.enterprise.credential.CallerOnlyCredential;
import javax.security.enterprise.identitystore.CredentialValidationResult;
import javax.security.enterprise.identitystore.IdentityStoreHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static javax.security.enterprise.identitystore.CredentialValidationResult.Status.VALID;
public class Oauth2Authentication implements HttpAuthenticationMechanism {
private static final Pattern CHALLENGE_PATTERN
= Pattern.compile("^Bearer *([^ ]+) *$", Pattern.CASE_INSENSITIVE);
private IdentityStoreHandler identityStoreHandler;
name = "keyvalue") (
private KeyValueTemplate template;
public AuthenticationStatus validateRequest(HttpServletRequest request, HttpServletResponse response,
HttpMessageContext httpMessageContext) {
final String authorization = request.getHeader("Authorization");
Matcher matcher = CHALLENGE_PATTERN.matcher(Optional.ofNullable(authorization).orElse(""));
if (!matcher.matches()) {
return httpMessageContext.doNothing();
}
final String token = matcher.group(1);
final Optional<AccessToken> optional = template.get(AccessToken.PREFIX + token, AccessToken.class);
if (!optional.isPresent()) {
return httpMessageContext.responseUnauthorized();
}
final AccessToken accessToken = optional.get();
final CredentialValidationResult validate = identityStoreHandler.validate(new CallerOnlyCredential(accessToken.getUser()));
if (validate.getStatus() == VALID) {
return httpMessageContext.notifyContainerAboutLogin(validate.getCallerPrincipal(), validate.getCallerGroups());
} else {
return httpMessageContext.responseUnauthorized();
}
}
}
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.
xxxxxxxxxx
import jakarta.nosql.mapping.keyvalue.KeyValueTemplate;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import sh.platform.sample.security.RemoveToken;
import sh.platform.sample.security.RemoveUser;
import sh.platform.sample.security.User;
import sh.platform.sample.security.UserForbiddenException;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
class Oauth2Observes {
name = "keyvalue") (
private KeyValueTemplate template;
public void observe( RemoveUser removeUser) {
//....
}
public void observe( RemoveToken removeToken) {
//....
}
}
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:
xxxxxxxxxx
mongodb
type mongodb3.6
disk1024
redis
type redis-persistent5.0
disk1024
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.
xxxxxxxxxx
name app
type"java:11"
disk1024
hooks
build mvn clean package payara-micro bundle
relationships
mongodb'mongodb:mongodb'
redis'redis:redis'
web
commands
start
export MONGO_PORT=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].port"`
export MONGO_HOST=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].host"`
export MONGO_ADDRESS="${MONGO_HOST}:${MONGO_PORT}"
export MONGO_PASSWORD=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].password"`
export MONGO_USER=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].username"`
export MONGO_DATABASE=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].path"`
export REDIS_HOST=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".redis[0].host"`
java -jar -Xmx$(jq .info.limits.memory /run/config.json)m -XX:+ExitOnOutOfMemoryError \
-Ddocument.settings.jakarta.nosql.host=$MONGO_ADDRESS \
-Ddocument.database=$MONGO_DATABASE -Ddocument.settings.jakarta.nosql.user=$MONGO_USER \
-Ddocument.settings.jakarta.nosql.password=$MONGO_PASSWORD \
-Ddocument.settings.mongodb.authentication.source=$MONGO_DATABASE \
-Dkeyvalue.settings.jakarta.nosql.host=$REDIS_HOST \
target/microprofile-microbundle.jar --port $PORT
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.
Opinions expressed by DZone contributors are their own.
Comments