Skip to content

Commit b8eaae0

Browse files
committed
remove generic soft-delete implementation from all tables
1 parent 0b34d07 commit b8eaae0

26 files changed

Lines changed: 161 additions & 619 deletions

src/integrationTest/java/com/trynoice/api/identity/AccountControllerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
44
import com.trynoice.api.identity.entities.AuthUser;
5+
import com.trynoice.api.identity.entities.RefreshToken;
56
import com.trynoice.api.identity.exceptions.SignInTokenDispatchException;
67
import com.trynoice.api.identity.payload.AuthCredentialsResponse;
78
import com.trynoice.api.identity.payload.SignInParams;
@@ -40,7 +41,6 @@
4041
import static org.junit.jupiter.api.Assertions.assertNotEquals;
4142
import static org.junit.jupiter.api.Assertions.assertNotNull;
4243
import static org.junit.jupiter.api.Assertions.assertNull;
43-
import static org.junit.jupiter.api.Assertions.assertTrue;
4444
import static org.junit.jupiter.params.provider.Arguments.arguments;
4545
import static org.mockito.ArgumentMatchers.any;
4646
import static org.mockito.ArgumentMatchers.eq;
@@ -357,7 +357,7 @@ void deleteAccount() throws Exception {
357357
.andExpect(status().is(HttpStatus.NO_CONTENT.value()));
358358

359359
// validate that all existing refresh tokens have been revoked.
360-
assertTrue(refreshTokens.stream().noneMatch(t -> t.getDeletedAt() != null));
360+
refreshTokens.forEach(t -> assertNull(entityManager.find(RefreshToken.class, t.getId())));
361361

362362
// perform the request again to ensure access token no longer works
363363
mockMvc.perform(

src/integrationTest/java/com/trynoice/api/platform/BasicEntityRepositoryTest.java

Lines changed: 0 additions & 150 deletions
This file was deleted.

src/integrationTest/java/com/trynoice/api/platform/TestConfig.java

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/integrationTest/java/com/trynoice/api/platform/TestEntity.java

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/integrationTest/java/com/trynoice/api/platform/TestEntityRepository.java

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/integrationTest/java/com/trynoice/api/subscription/SubscriptionTestUtils.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,13 @@ public static Subscription buildSubscription(
6666

6767
@NonNull
6868
static Customer buildCustomer(@NonNull EntityManager entityManager, @NonNull AuthUser user) {
69-
val customer = Customer.builder()
70-
.userId(user.getId())
71-
.stripeId(UUID.randomUUID().toString())
72-
.build();
69+
val customer = Optional.ofNullable(entityManager.find(Customer.class, user.getId()))
70+
.orElse(
71+
Customer.builder()
72+
.userId(user.getId())
73+
.build());
7374

75+
customer.setStripeId(UUID.randomUUID().toString());
7476
return entityManager.merge(customer);
7577
}
7678

src/integrationTest/java/com/trynoice/api/testing/AuthTestUtils.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,18 @@ public static String createSignedRefreshJwt(
9292

9393
val jwtSigningAlgorithm = Algorithm.HMAC256(hmacSecret);
9494
if (type != JwtType.REUSED) {
95-
return refreshToken.getJwt(jwtSigningAlgorithm);
95+
return refreshToken.toSignedJwt(jwtSigningAlgorithm);
9696
}
9797

9898
// create a separate entity that is not attached to the entity manager.
99-
val usedRefreshToken = RefreshToken.builder()
99+
return RefreshToken.builder()
100+
.id(refreshToken.getId())
101+
.createdAt(refreshToken.getCreatedAt())
100102
.owner(owner)
101103
.expiresAt(refreshToken.getExpiresAt())
102104
.ordinal(refreshToken.getOrdinal() + 1)
103-
.build();
104-
105-
usedRefreshToken.setId(refreshToken.getId());
106-
usedRefreshToken.setCreatedAt(refreshToken.getCreatedAt());
107-
108-
return usedRefreshToken.getJwt(jwtSigningAlgorithm);
105+
.build()
106+
.toSignedJwt(jwtSigningAlgorithm);
109107
}
110108

111109
/**

src/main/java/com/trynoice/api/identity/AccountService.java

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ private String createSignInToken(@NonNull AuthUser authUser) throws TooManySignI
144144
.owner(authUser)
145145
.expiresAt(OffsetDateTime.now().plus(authConfig.getSignInTokenExpiry()))
146146
.build())
147-
.getJwt(jwtAlgorithm);
147+
.toSignedJwt(jwtAlgorithm);
148148
}
149149

150150
/**
@@ -158,7 +158,8 @@ private String createSignInToken(@NonNull AuthUser authUser) throws TooManySignI
158158
@Transactional(rollbackFor = Throwable.class)
159159
public void signOut(@NonNull String refreshJwt, @NonNull String accessJwt) throws RefreshTokenVerificationException {
160160
val refreshToken = verifyRefreshJWT(refreshJwt);
161-
refreshTokenRepository.delete(refreshToken);
161+
refreshToken.setExpiresAt(OffsetDateTime.now());
162+
refreshTokenRepository.save(refreshToken);
162163
revokedAccessJwtCache.put(accessJwt, Boolean.TRUE);
163164
}
164165

@@ -175,29 +176,29 @@ public void signOut(@NonNull String refreshJwt, @NonNull String accessJwt) throw
175176
@Transactional(rollbackFor = Throwable.class, noRollbackFor = RefreshTokenVerificationException.class)
176177
public AuthCredentialsResponse issueAuthCredentials(@NonNull String refreshToken, String userAgent) throws RefreshTokenVerificationException {
177178
var token = verifyRefreshJWT(refreshToken);
179+
val owner = token.getOwner();
180+
owner.updateLastActiveTimestamp(); // update last active timestamp for the user and save.
178181

179182
// ordinal 0 implies that this refresh token is being used to sign in, so persist userAgent
180183
// and reset sign-in attempts.
181184
if (Long.valueOf(0).equals(token.getOrdinal())) {
182185
token.setUserAgent(requireNonNullElse(userAgent, ""));
183-
token.getOwner().resetSignInAttemptData();
186+
owner.resetSignInAttemptData();
184187
}
185188

186-
// saving AuthUser entity implicitly updates last active timestamp, so always perform the
187-
// save step regardless of the token ordinal value.
188-
authUserRepository.save(token.getOwner());
189+
authUserRepository.save(owner);
189190
token.setExpiresAt(OffsetDateTime.now().plus(authConfig.getRefreshTokenExpiry()));
190191
token.incrementOrdinal();
191192
token = refreshTokenRepository.save(token);
192193

193194
val accessTokenExpiry = OffsetDateTime.now().plus(authConfig.getAccessTokenExpiry());
194195
val signedAccessToken = JWT.create()
195-
.withSubject("" + token.getOwner().getId())
196+
.withSubject("" + owner.getId())
196197
.withExpiresAt(Date.from(accessTokenExpiry.toInstant()))
197198
.sign(jwtAlgorithm);
198199

199200
return AuthCredentialsResponse.builder()
200-
.refreshToken(token.getJwt(jwtAlgorithm))
201+
.refreshToken(token.toSignedJwt(jwtAlgorithm))
201202
.accessToken(signedAccessToken)
202203
.build();
203204
}
@@ -216,10 +217,17 @@ private RefreshToken verifyRefreshJWT(@NonNull String jwt) throws RefreshTokenVe
216217
val token = refreshTokenRepository.findById(jwtId)
217218
.orElseThrow(() -> new RefreshTokenVerificationException("refresh token doesn't exist in database"));
218219

220+
// an edge case could be that we revoked the token in the database, but the client didn't
221+
// receive the updated JWT. Currently, it happens when the token ordinals don't match.
222+
if (token.getExpiresAt().isBefore(OffsetDateTime.now())) {
223+
throw new RefreshTokenVerificationException("refresh token has expired");
224+
}
225+
219226
// if token ordinal is different, it implies that an old refresh token is being re-used.
220227
// delete token on re-use to effectively sign out both the legitimate user and the attacker.
221228
if (token.getOrdinal() != jwtOrdinal) {
222-
refreshTokenRepository.delete(token);
229+
token.setExpiresAt(OffsetDateTime.now());
230+
refreshTokenRepository.save(token);
223231
throw new RefreshTokenVerificationException("refresh token ordinal mismatch");
224232
}
225233

0 commit comments

Comments
 (0)