Skip to content

Commit c1538c3

Browse files
authored
Merge pull request #23 from MEITREX/schurpl-testing
Fix 200 status error response for refreshing access tokens
2 parents 625c984 + f624e5b commit c1538c3

File tree

3 files changed

+43
-14
lines changed

3 files changed

+43
-14
lines changed

src/main/java/de/unistuttgart/iste/meitrex/user_service/service/AccessTokenService.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public class AccessTokenService {
4444
private final ExternalOAuthClient externalOAuthClient;
4545

4646
/**
47-
* Checks if a valid access token or refresh token is available for a given user and provider.
47+
* Checks if a valid access token is available for a given user and provider.
4848
*
4949
* @param currentUser the currently logged-in user.
5050
* @param providerDto the external service provider name.
@@ -68,8 +68,11 @@ public boolean isAccessTokenAvailable(LoggedInUser currentUser, ExternalServiceP
6868
return true;
6969
}
7070

71-
// If access token expired, check if the refresh token is expired
72-
return accessToken.getRefreshTokenExpiresAt().isAfter(now);
71+
// If access token expired, check if the refresh token is expired and try to refresh it
72+
// There's a bug in GH Api that returns 200 error response because it thinks there is a problem with the refresh token (there is not), so we check here if the refresh works.
73+
// if it does, then refreshAccessToken returns true and a new access token is saved, so getAccessToken can return it (without needing to refresh).
74+
// if it doesn't, then returns false and the user will be prompted to re-authorize GitHub. (using generateAccessToken)
75+
return accessToken.getRefreshTokenExpiresAt().isAfter(now) && refreshAccessToken(accessToken, provider) != null;
7376
}
7477

7578
/**
@@ -94,10 +97,14 @@ public AccessToken getAccessToken(UUID currentUserId, ExternalServiceProviderDto
9497
AccessTokenEntity accessToken = accessTokenOptional.get();
9598
OffsetDateTime now = OffsetDateTime.now();
9699

100+
97101
if (accessToken.getAccessTokenExpiresAt() == null || accessToken.getAccessTokenExpiresAt().isAfter(now)) {
98102
return new AccessToken(accessToken.getAccessToken(), accessToken.getExternalUserId());
99103
}
100104

105+
// the lines below won't run in current setup, since if we call getAccessToken, we already refreshed the access token if needed in isAccessTokenAvailable.
106+
// but if Github resolve their problem, the normal workflow should be used instead, i.e. in isAccessTokenAvailable, we only check if the access token is expired without refreshing.
107+
// The refresh will be done below then (check the isAccessTokenAvailable method for more details)
101108
if (!accessToken.getRefreshTokenExpiresAt().isAfter(now)) {
102109
throw new EntityNotFoundException("Access token expired and refresh token expired for user " + currentUserInfo.getId() + " and provider " + provider);
103110
}
@@ -123,7 +130,7 @@ private AccessToken refreshAccessToken(AccessTokenEntity accessToken, ExternalSe
123130
return new AccessToken(accessToken.getAccessToken(), accessToken.getExternalUserId());
124131
} catch (IOException | InterruptedException e) {
125132
if (e instanceof InterruptedException) Thread.currentThread().interrupt();
126-
log.error("Failed to refresh access token for user {} and provider {}", accessToken.getUserId(), provider, e);
133+
log.error("Failed to refresh access token for user {} and provider {}", accessToken.getUserId(), provider);
127134
}
128135
return null;
129136
}
@@ -161,7 +168,7 @@ public boolean generateAccessToken(LoggedInUser currentUser, GenerateAccessToken
161168
}
162169
} catch (IOException | InterruptedException e) {
163170
if (e instanceof InterruptedException) Thread.currentThread().interrupt();
164-
log.error("Failed to generate access token for user {} and provider {}", currentUserInfo.getId(), provider, e);
171+
log.error("Failed to generate access token for user {} and provider {}", currentUserInfo.getId(), provider);
165172
}
166173
return false;
167174
}

src/main/java/de/unistuttgart/iste/meitrex/user_service/service/oauth/GitHubOAuthStrategy.java

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,17 @@ public AccessTokenResponse exchangeCodeForAccessToken(String code) throws IOExce
8484

8585
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
8686

87-
if (response.statusCode() == 200) {
88-
return parseTokenResponse(response.body());
87+
if (response.statusCode() != 200) {
88+
log.error("Failed to exchange code. HTTP {} Body: {}", response.statusCode(), response.body());
89+
return null;
8990
}
9091

91-
log.error("Failed to exchange code for access token. HTTP Status: {} Response: {}", response.statusCode(), response.body());
92-
return null;
92+
try {
93+
return parseTokenResponse(response.body());
94+
} catch (Exception ex) {
95+
log.error("Non-JSON or malformed token response body: {}", response.body());
96+
return null;
97+
}
9398
}
9499

95100
@Override
@@ -108,14 +113,25 @@ public AccessTokenResponse refreshAccessToken(String refreshToken) throws IOExce
108113

109114
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
110115

111-
if (response.statusCode() == 200) {
112-
return parseTokenResponse(response.body());
116+
if (response.statusCode() != 200) {
117+
log.error("Failed to refresh token. HTTP {} Body: {}", response.statusCode(), response.body());
118+
return null;
113119
}
114120

115-
log.error("Failed to refresh access token. HTTP Status: {} Response: {}", response.statusCode(), response.body());
116-
return null;
121+
try {
122+
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
123+
if (json.has("error")) {
124+
log.error("GitHub refresh error: {}", json);
125+
return null;
126+
}
127+
return parseTokenResponse(response.body());
128+
} catch (Exception ex) {
129+
log.error("Non-JSON or malformed refresh response body: {}", response.body());
130+
return null;
131+
}
117132
}
118133

134+
119135
public String fetchExternalUserId(String accessToken) throws IOException, InterruptedException {
120136
HttpRequest request = HttpRequest.newBuilder()
121137
.uri(URI.create(githubInfo().getExternalUserIdUrl()))

src/test/java/de/unistuttgart/iste/meitrex/user_service/service/AccessTokenServiceTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static org.mockito.Mockito.*;
2828
import static org.junit.jupiter.api.Assertions.*;
2929
import static org.mockito.ArgumentMatchers.any;
30+
import static org.mockito.ArgumentMatchers.eq;
3031

3132
class AccessTokenServiceTest {
3233

@@ -100,13 +101,18 @@ void testIsAccessTokenAvailable_ValidExpiringToken() {
100101
}
101102

102103
@Test
103-
void testIsAccessTokenAvailable_ExpiredToken_ValidRefreshToken() {
104+
void testIsAccessTokenAvailable_ExpiredToken_ValidRefreshToken() throws Exception{
104105
validAccessToken.setAccessToken("valid_access_token");
105106
validAccessToken.setAccessTokenExpiresAt(OffsetDateTime.now().minusMinutes(5));
106107
validAccessToken.setRefreshToken("refresh_token");
107108
validAccessToken.setRefreshTokenExpiresAt(OffsetDateTime.now().plusMinutes(5));
108109
when(accessTokenRepository.findByUserIdAndProvider(any(), any())).thenReturn(Optional.of(validAccessToken));
109110

111+
AccessTokenResponse refreshed = new AccessTokenResponse(
112+
"new_valid_access_token", /* expiresIn */ 3600, "new_refresh", /* refreshExpiresIn */ 7200);
113+
when(externalOAuthClient.refreshAccessToken(eq("refresh_token"), any()))
114+
.thenReturn(refreshed);
115+
110116
Boolean result = accessTokenService.isAccessTokenAvailable(loggedInUser, providerDto);
111117

112118
assertThat(result, is(true));

0 commit comments

Comments
 (0)