Skip to content

Commit

Permalink
enhancements(test): added basic test for testing of auth endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
zZHorizonZz committed Aug 1, 2023
1 parent 893688c commit e4797ad
Show file tree
Hide file tree
Showing 14 changed files with 329 additions and 63 deletions.
2 changes: 1 addition & 1 deletion server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
<dependency>
<groupId>org.instancio</groupId>
<artifactId>instancio-junit</artifactId>
<version>2.9.0</version>
<version>3.0.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public class AccountEntity extends PanacheEntityBase {
@Column(name = "session_state")
private String sessionState;

@ManyToOne
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private UserEntity user;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class RefreshTokenEntity extends PanacheEntityBase {
@Column(name = "updated_at")
private Date updatedAt;

@ManyToOne
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private UserEntity user;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,6 @@ public class UserEntity extends PanacheEntityBase {
@Column(name = "password")
private String password;

@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private Set<AccountEntity> accounts;

@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private Set<RefreshTokenEntity> refreshTokens;

public UserEntity() {
}
}
17 changes: 17 additions & 0 deletions server/src/main/java/dev/shiperist/exception/ErrorMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.shiperist.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class ErrorMessage {

public static final ErrorMessage EMAIL_ALREADY_EXISTS = new ErrorMessage("email", "Email already exists");
public static final ErrorMessage INVALID_CREDENTIALS = new ErrorMessage("credentials", "Invalid credentials");
public static final ErrorMessage INVALID_GRANT_TYPE = new ErrorMessage("grant_type", "Invalid grant type");
public static final ErrorMessage INVALID_REFRESH_TOKEN = new ErrorMessage("refresh_token", "Invalid refresh token");

private final String error;
private final String description;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.shiperist.exception;

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import org.hibernate.exception.ConstraintViolationException;

@Provider
public class HibernateExceptionHandler implements ExceptionMapper<ConstraintViolationException> {

@Override
public Response toResponse(ConstraintViolationException e) {
if (e.getConstraintName().contains("email")) {
return Response.status(Response.Status.BAD_REQUEST).entity(ErrorMessage.EMAIL_ALREADY_EXISTS).build();
}

return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.shiperist.exception;

import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

@Provider
public class NotFoundExceptionHandler implements ExceptionMapper<NotFoundException> {

@Override
public Response toResponse(NotFoundException e) {
if (e.getMessage().contains("Refresh token not found")) {
return Response.status(Response.Status.BAD_REQUEST).entity(ErrorMessage.INVALID_REFRESH_TOKEN).build();
}

return Response.status(Response.Status.NOT_FOUND).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@ public class Account {
private String scope;
private String idToken;
private String sessionState;
private UserEntity user;
}
2 changes: 0 additions & 2 deletions server/src/main/java/dev/shiperist/model/account/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,4 @@ public class User {
private Date emailVerified;
private String image;
public String password;
private Set<AccountEntity> accounts;
private Set<RefreshTokenEntity> sessions;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;

import java.util.List;

@WithSession
@ApplicationScoped
public class RefreshTokenRepository implements PanacheRepositoryBase<RefreshTokenEntity, Long> {

public Uni<RefreshTokenEntity> findByToken(String token) {
return find("sessionToken", token).firstResult();
return find("token", token).firstResult();
}

public Uni<Boolean> revokeAllForUser(Long userId) {
return update("revoked = true where userId = ?1", userId).map(updateResult -> updateResult > 0);
}
}
140 changes: 101 additions & 39 deletions server/src/main/java/dev/shiperist/resource/AuthResource.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
package dev.shiperist.resource;

import dev.shiperist.exception.ErrorMessage;
import dev.shiperist.model.account.User;
import dev.shiperist.model.account.UserResponse;
import dev.shiperist.service.account.AccountService;
import dev.shiperist.service.account.TokenService;
import dev.shiperist.service.account.UserService;
import dev.shiperist.util.SecurityUtil;
import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni;
import jakarta.annotation.security.PermitAll;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;

import java.util.Date;
import java.util.Optional;

import static io.netty.handler.codec.http.HttpHeaders.Values.APPLICATION_JSON;

@RequestScoped
@Path("/auth")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
Expand All @@ -31,6 +42,10 @@ public class AuthResource {
@Inject
TokenService tokenService;

@Inject
@Claim(standard = Claims.sub)
String sub;

@GET
@PermitAll
@Path("/settings")
Expand All @@ -42,8 +57,19 @@ public Uni<Response> getSettings() {
@POST
@PermitAll
@Path("/signup")
public Uni<User> signUp(User user) {
return userService.createUser(user.getName(), user.getEmail(), user.getImage(), user.getPassword());
@Operation(summary = "Signs up a new user")
@APIResponse(
responseCode = "200",
description = "The created user",
content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = User.class, required = true))
)
@APIResponse(
responseCode = "400",
description = "The user with the given email already exists"
)
public Uni<Response> signUp(User user) {
return userService.createUser(user.getName(), user.getEmail(), user.getImage(), user.getPassword())
.onItem().ifNotNull().transform(u -> Response.status(Response.Status.CREATED).entity(u).build());
}

@POST
Expand All @@ -66,6 +92,16 @@ public Uni<Response> recoverPassword(User user) {
@PermitAll
@Path("/token")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Operation(summary = "Returns a new access token")
@APIResponse(
responseCode = "200",
description = "The response containing the access token",
content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = UserResponse.class, required = true))
)
@APIResponse(
responseCode = "401",
description = "Invalid email or password"
)
public Uni<Response> getToken(@FormParam("grant_type") String grantType,
@FormParam("email") String email,
@FormParam("password") String password,
Expand All @@ -75,67 +111,93 @@ public Uni<Response> getToken(@FormParam("grant_type") String grantType,
return switch (grantType) {
case "password" -> userService.getUserByEmail(email)
.flatMap(user -> {
if (user.isEmpty()) {
if (user == null) {
return Uni.createFrom().item(Response.status(Response.Status.UNAUTHORIZED)
.entity("Invalid email or password.").build());
.entity(ErrorMessage.INVALID_CREDENTIALS).build());
}

User validUser = user.get();

if (userService.checkPassword(password, validUser.getPassword())) {
return tokenService.createRefreshToken(validUser)
if (userService.checkPassword(password, user.getPassword())) {
return tokenService.createRefreshToken(user)
.map(token -> {
response.setAccessToken(SecurityUtil.generateToken(validUser, new Date()));
response.setAccessToken(SecurityUtil.generateToken(user, new Date()));
response.setTokenType("bearer");
response.setExpiresIn(3600);
response.setRefreshToken(token.getToken());
return Response.ok(response).build();
});
} else {
return Uni.createFrom().item(Response.status(Response.Status.UNAUTHORIZED)
.entity("Invalid email or password.").build());
.entity(ErrorMessage.INVALID_CREDENTIALS).build());
}
});
case "refresh_token" ->
// Assuming the refresh_token is the sessionId.
tokenService.swapRefreshToken(refreshToken)
.flatMap(token -> userService.getUser(token.getUserId()))
.map(user -> {
if (user.isEmpty()) {
return Response.status(Response.Status.UNAUTHORIZED)
.entity("Invalid refresh token.").build();
}

User validUser = user.get();

response.setAccessToken(SecurityUtil.generateToken(validUser, new Date()));
response.setTokenType("bearer");
response.setExpiresIn(3600);
response.setRefreshToken(refreshToken);

return Response.ok(response).build();
});
case "refresh_token" -> tokenService.swapRefreshToken(refreshToken)
.flatMap(token -> userService.getUser(token.getUserId()))
.map(user -> {
response.setAccessToken(SecurityUtil.generateToken(user, new Date()));
response.setTokenType("bearer");
response.setExpiresIn(3600);
response.setRefreshToken(refreshToken);

return Response.ok(response).build();
});
default -> Uni.createFrom().item(Response.status(Response.Status.BAD_REQUEST)
.entity("Invalid grant_type parameter.").build());
.entity(ErrorMessage.INVALID_GRANT_TYPE).build());
};
}

@GET
@Authenticated
@Path("/user/{id}")
public Uni<Optional<User>> getUser(@PathParam("id") Long id) {
return userService.getUser(id);
@Operation(summary = "Returns user information")
@APIResponse(
responseCode = "200",
description = "The user information",
content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = User.class, required = true))
)
@APIResponse(
responseCode = "403",
description = "Forbidden"
)
public Uni<Response> getUser(@PathParam("id") Long id) {
if (id == null || !id.equals(Long.parseLong(sub))) {
return Uni.createFrom().item(Response.status(Response.Status.FORBIDDEN).build());
}
return userService.getUser(id)
.map(user -> Response.ok(user).build());
}

@PUT
@Authenticated
@Path("/user")
public Uni<User> updateUser(User user) {
return userService.updateUser(user);
@Operation(summary = "Updates user information")
@APIResponse(
responseCode = "200",
description = "The updated user",
content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = User.class, required = true))
)
@APIResponse(
responseCode = "403",
description = "Forbidden"
)
public Uni<Response> updateUser(User user) {
if (user.getId() == null || !user.getId().equals(Long.parseLong(sub))) {
return Uni.createFrom().item(Response.status(Response.Status.FORBIDDEN).build());
}

return userService.updateUser(user)
.map(updatedUser -> Response.ok(updatedUser).build());
}

@POST
@HEAD
@Authenticated
@Path("/logout")
public Uni<Response> logoutUser(User user) {
// Implement the logic to logout the user (invalidate all sessions).
return Uni.createFrom().item(Response.ok().build());
@Operation(summary = "Logs out the user")
@APIResponse(
responseCode = "200",
description = "Logout successful"
)
public Uni<Response> logoutUser() {
return tokenService.revokeAllRefreshTokensForUser(Long.parseLong(sub))
.map(ignored -> Response.ok().build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;

@ApplicationScoped
public class TokenService {
Expand All @@ -34,6 +35,10 @@ public Uni<RefreshToken> createRefreshToken(User user) {
public Uni<RefreshToken> swapRefreshToken(String token) {
return refreshTokenRepository.findByToken(token)
.flatMap(refreshToken -> {
if(refreshToken == null) {
return Uni.createFrom().failure(new NotFoundException("Refresh token not found"));
}

if (refreshToken.isRevoked()) {
return Uni.createFrom().failure(new RuntimeException("Refresh token is revoked"));
}
Expand All @@ -58,4 +63,9 @@ public Uni<Boolean> revokeRefreshToken(String token) {
return refreshTokenRepository.persist(refreshToken).map(refreshTokenMapper::toDomain);
}).map(RefreshToken::isRevoked);
}

@WithTransaction
public Uni<Boolean> revokeAllRefreshTokensForUser(Long userId) {
return refreshTokenRepository.revokeAllForUser(userId);
}
}
Loading

0 comments on commit e4797ad

Please sign in to comment.