Skip to content
This repository was archived by the owner on Jun 30, 2023. It is now read-only.

Commit 1b51f85

Browse files
Clément Denistangiel
authored andcommitted
Add retry logic in GoogleAuth.getTokenInfoRemote to handle 5xx errors
1 parent 36333f9 commit 1b51f85

8 files changed

Lines changed: 101 additions & 35 deletions

File tree

endpoints-framework/src/main/java/com/google/api/server/spi/auth/EndpointsAuthenticator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.api.server.spi.config.Authenticator;
2121
import com.google.api.server.spi.config.Singleton;
2222
import com.google.api.server.spi.request.Attribute;
23+
import com.google.api.server.spi.response.ServiceUnavailableException;
2324
import com.google.common.annotations.VisibleForTesting;
2425

2526
import javax.servlet.http.HttpServletRequest;
@@ -51,7 +52,7 @@ public EndpointsAuthenticator(GoogleJwtAuthenticator jwtAuthenticator,
5152
}
5253

5354
@Override
54-
public User authenticate(HttpServletRequest request) {
55+
public User authenticate(HttpServletRequest request) throws ServiceUnavailableException {
5556
Attribute attr = Attribute.from(request);
5657
User user = jwtAuthenticator.authenticate(request);
5758
if (user == null) {

endpoints-framework/src/main/java/com/google/api/server/spi/auth/GoogleAppEngineAuthenticator.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.api.server.spi.config.model.ApiMethodConfig;
2323
import com.google.api.server.spi.config.scope.AuthScopeExpression;
2424
import com.google.api.server.spi.request.Attribute;
25+
import com.google.api.server.spi.response.ServiceUnavailableException;
2526
import com.google.appengine.api.oauth.OAuthRequestException;
2627
import com.google.appengine.api.oauth.OAuthService;
2728
import com.google.appengine.api.oauth.OAuthServiceFactory;
@@ -56,7 +57,7 @@ public GoogleAppEngineAuthenticator(OAuthService oauthService, UserService userS
5657
}
5758

5859
@VisibleForTesting
59-
String getOAuth2ClientIdDev(String token) {
60+
String getOAuth2ClientIdDev(String token) throws ServiceUnavailableException {
6061
GoogleAuth.TokenInfo tokenInfo = GoogleAuth.getTokenInfoRemote(token);
6162
return tokenInfo != null ? tokenInfo.clientId : null;
6263
}
@@ -68,7 +69,7 @@ boolean shouldTryCookieAuth(ApiMethodConfig config) {
6869

6970
@VisibleForTesting
7071
com.google.appengine.api.users.User getOAuth2User(HttpServletRequest request,
71-
ApiMethodConfig config) {
72+
ApiMethodConfig config) throws ServiceUnavailableException {
7273
String token = GoogleAuth.getAuthToken(request);
7374
if (!GoogleAuth.isOAuth2Token(token)) {
7475
return null;
@@ -114,7 +115,7 @@ com.google.appengine.api.users.User getOAuth2User(HttpServletRequest request,
114115
}
115116

116117
@Override
117-
public User authenticate(HttpServletRequest request) {
118+
public User authenticate(HttpServletRequest request) throws ServiceUnavailableException {
118119
Attribute attr = Attribute.from(request);
119120
if (!EnvUtil.isRunningOnAppEngine()) {
120121
return null;

endpoints-framework/src/main/java/com/google/api/server/spi/auth/GoogleAuth.java

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616
package com.google.api.server.spi.auth;
1717

1818
import com.google.api.client.http.GenericUrl;
19+
import com.google.api.client.http.HttpIOExceptionHandler;
1920
import com.google.api.client.http.HttpRequest;
21+
import com.google.api.client.http.HttpResponse;
22+
import com.google.api.client.http.HttpUnsuccessfulResponseHandler;
2023
import com.google.api.client.util.Key;
2124
import com.google.api.server.spi.Client;
2225
import com.google.api.server.spi.Constant;
2326
import com.google.api.server.spi.Strings;
2427
import com.google.api.server.spi.request.Attribute;
28+
import com.google.api.server.spi.response.ServiceUnavailableException;
2529
import com.google.common.annotations.VisibleForTesting;
2630
import com.google.common.collect.ImmutableList;
2731

@@ -172,29 +176,66 @@ public static class TokenInfo {
172176
@Key("issued_to") public String clientId;
173177
@Key("scope") public String scopes;
174178
@Key("user_id") public String userId;
179+
@Key("error_description") public String errorDescription;
175180
}
176181

177182
/**
178183
* Get OAuth2 token info from remote token validation API.
184+
* Retries IOExceptions and 5xx responses once.
179185
*/
180-
static TokenInfo getTokenInfoRemote(String token) {
186+
static TokenInfo getTokenInfoRemote(String token) throws ServiceUnavailableException {
181187
try {
182188
HttpRequest request = Client.getInstance().getJsonHttpRequestFactory()
183189
.buildGetRequest(new GenericUrl(TOKEN_INFO_ENDPOINT + token));
190+
configureErrorHandling(request);
184191
return parseTokenInfo(request);
185192
} catch (IOException e) {
186-
logger.log(Level.WARNING, "Failed to retrieve tokeninfo", e);
187-
return null;
193+
throw new ServiceUnavailableException("Failed to perform access token validation", e);
188194
}
189195
}
190196

191197
@VisibleForTesting
192-
static TokenInfo parseTokenInfo(HttpRequest request) throws IOException {
193-
TokenInfo info = request.execute().parseAs(TokenInfo.class);
198+
static TokenInfo parseTokenInfo(HttpRequest request)
199+
throws IOException, ServiceUnavailableException {
200+
HttpResponse response = request.execute();
201+
int statusCode = response.getStatusCode();
202+
TokenInfo info = response.parseAs(TokenInfo.class);
203+
if (statusCode != 200) {
204+
String errorDescription = "Unknown error";
205+
if (info != null && info.errorDescription != null) {
206+
errorDescription = info.errorDescription;
207+
}
208+
errorDescription += " (" + statusCode + ")";
209+
if (statusCode >= 500) {
210+
logger.log(Level.SEVERE, "Error validating access token: " + errorDescription);
211+
throw new ServiceUnavailableException("Failed to validate access token");
212+
}
213+
logger.log(Level.INFO, "Invalid access token: " + errorDescription);
214+
return null;
215+
}
194216
if (info == null || Strings.isEmptyOrWhitespace(info.email)) {
195217
logger.log(Level.WARNING, "Access token does not contain email scope");
196218
return null;
197219
}
198220
return info;
199221
}
222+
223+
@VisibleForTesting
224+
static void configureErrorHandling(HttpRequest request) {
225+
request.setNumberOfRetries(1)
226+
.setThrowExceptionOnExecuteError(false)
227+
.setIOExceptionHandler(new HttpIOExceptionHandler() {
228+
@Override
229+
public boolean handleIOException(HttpRequest request, boolean supportsRetry) {
230+
return true; // consider all IOException as transient
231+
}
232+
})
233+
.setUnsuccessfulResponseHandler(new HttpUnsuccessfulResponseHandler() {
234+
@Override
235+
public boolean handleResponse(HttpRequest request, HttpResponse response,
236+
boolean supportsRetry) {
237+
return response.getStatusCode() >= 500; // only retry Google's backend errors
238+
}
239+
});
240+
}
200241
}

endpoints-framework/src/main/java/com/google/api/server/spi/auth/GoogleOAuth2Authenticator.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.api.server.spi.config.Singleton;
2323
import com.google.api.server.spi.config.model.ApiMethodConfig;
2424
import com.google.api.server.spi.request.Attribute;
25+
import com.google.api.server.spi.response.ServiceUnavailableException;
2526
import com.google.common.annotations.VisibleForTesting;
2627
import com.google.common.collect.ImmutableSet;
2728

@@ -39,7 +40,7 @@ public class GoogleOAuth2Authenticator implements Authenticator {
3940
private static final Logger logger = Logger.getLogger(GoogleOAuth2Authenticator.class.getName());
4041

4142
@Override
42-
public User authenticate(HttpServletRequest request) {
43+
public User authenticate(HttpServletRequest request) throws ServiceUnavailableException {
4344
Attribute attr = Attribute.from(request);
4445
if (attr.isEnabled(Attribute.SKIP_TOKEN_AUTH)) {
4546
return null;
@@ -89,7 +90,7 @@ public User authenticate(HttpServletRequest request) {
8990
}
9091

9192
@VisibleForTesting
92-
TokenInfo getTokenInfoRemote(String token) {
93+
TokenInfo getTokenInfoRemote(String token) throws ServiceUnavailableException {
9394
return GoogleAuth.getTokenInfoRemote(token);
9495
}
9596
}

endpoints-framework/src/test/java/com/google/api/server/spi/auth/EndpointsAuthenticatorTest.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.api.server.spi.EnvUtil;
2222
import com.google.api.server.spi.auth.common.User;
2323
import com.google.api.server.spi.request.Attribute;
24+
import com.google.api.server.spi.response.ServiceUnavailableException;
2425

2526
import org.junit.Before;
2627
import org.junit.Test;
@@ -53,13 +54,13 @@ public void setUp() throws Exception {
5354
}
5455

5556
@Test
56-
public void testAuthenticate_jwt() {
57+
public void testAuthenticate_jwt() throws ServiceUnavailableException {
5758
when(jwtAuthenticator.authenticate(request)).thenReturn(USER);
5859
assertEquals(USER, authenticator.authenticate(request));
5960
}
6061

6162
@Test
62-
public void testAuthenticate_appEngine() {
63+
public void testAuthenticate_appEngine() throws ServiceUnavailableException {
6364
when(jwtAuthenticator.authenticate(request)).thenReturn(null);
6465
when(appEngineAuthenticator.authenticate(request)).thenReturn(USER);
6566

@@ -70,7 +71,7 @@ public void testAuthenticate_appEngine() {
7071
}
7172

7273
@Test
73-
public void testAuthenticate_oauth2NonAppEngine() {
74+
public void testAuthenticate_oauth2NonAppEngine() throws ServiceUnavailableException {
7475
when(jwtAuthenticator.authenticate(request)).thenReturn(null);
7576
when(oauth2Authenticator.authenticate(request)).thenReturn(USER);
7677

@@ -81,7 +82,7 @@ public void testAuthenticate_oauth2NonAppEngine() {
8182
}
8283

8384
@Test
84-
public void testAuthenticate_oAuth2NotRequireAppEngineUser() {
85+
public void testAuthenticate_oAuth2NotRequireAppEngineUser() throws ServiceUnavailableException {
8586
when(jwtAuthenticator.authenticate(request)).thenReturn(null);
8687
when(oauth2Authenticator.authenticate(request)).thenReturn(USER);
8788

endpoints-framework/src/test/java/com/google/api/server/spi/auth/GoogleAppEngineAuthenticatorTest.java

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.api.server.spi.config.model.ApiMethodConfig;
2525
import com.google.api.server.spi.config.scope.AuthScopeExpressions;
2626
import com.google.api.server.spi.request.Attribute;
27+
import com.google.api.server.spi.response.ServiceUnavailableException;
2728
import com.google.appengine.api.oauth.OAuthRequestException;
2829
import com.google.appengine.api.oauth.OAuthService;
2930
import com.google.appengine.api.users.UserService;
@@ -87,7 +88,7 @@ private void initializeRequest(String bearerString) {
8788
}
8889

8990
@Test
90-
public void testGetOAuth2UserNonOAuth2() {
91+
public void testGetOAuth2UserNonOAuth2() throws ServiceUnavailableException {
9192
initializeRequest("Bearer badToken");
9293
assertNull(authenticator.getOAuth2User(request, config));
9394

@@ -152,27 +153,27 @@ public void testGetOAuth2UserSkipClientIdCheck() throws Exception {
152153
}
153154

154155
@Test
155-
public void testGetOAuth2UserAppEngineDevClientIdNotAllowed() {
156+
public void testGetOAuth2UserAppEngineDevClientIdNotAllowed() throws ServiceUnavailableException {
156157
System.setProperty(EnvUtil.ENV_APPENGINE_RUNTIME, "Developement");
157158
when(config.getScopeExpression()).thenReturn(AuthScopeExpressions.interpret(SCOPES));
158159
when(config.getClientIds()).thenReturn(ImmutableList.of("clientId2"));
159160
assertNull(authenticator.getOAuth2User(request, config));
160161
}
161162

162163
@Test
163-
public void testAuthenticateNonAppEngine() {
164+
public void testAuthenticateNonAppEngine() throws ServiceUnavailableException {
164165
System.clearProperty(EnvUtil.ENV_APPENGINE_RUNTIME);
165166
assertNull(authenticator.authenticate(request));
166167
}
167168

168169
@Test
169-
public void testAuthenticateSkipTokenAuth() {
170+
public void testAuthenticateSkipTokenAuth() throws ServiceUnavailableException {
170171
attr.set(Attribute.SKIP_TOKEN_AUTH, true);
171172
assertNull(authenticator.authenticate(request));
172173
}
173174

174175
@Test
175-
public void testAuthenticateOAuth2Fail() {
176+
public void testAuthenticateOAuth2Fail() throws ServiceUnavailableException {
176177
authenticator = new GoogleAppEngineAuthenticator(oauthService, userService) {
177178
@Override
178179
com.google.appengine.api.users.User getOAuth2User(HttpServletRequest request,
@@ -189,7 +190,7 @@ boolean shouldTryCookieAuth(ApiMethodConfig config) {
189190
}
190191

191192
@Test
192-
public void testAuthenticateOAuth2() {
193+
public void testAuthenticateOAuth2() throws ServiceUnavailableException {
193194
authenticator = new GoogleAppEngineAuthenticator(oauthService, userService) {
194195
@Override
195196
com.google.appengine.api.users.User getOAuth2User(HttpServletRequest request,
@@ -202,7 +203,7 @@ com.google.appengine.api.users.User getOAuth2User(HttpServletRequest request,
202203
}
203204

204205
@Test
205-
public void testAuthenticateSkipTokenAuthCookieAuthFail() {
206+
public void testAuthenticateSkipTokenAuthCookieAuthFail() throws ServiceUnavailableException {
206207
attr.set(Attribute.SKIP_TOKEN_AUTH, true);
207208
authenticator = new GoogleAppEngineAuthenticator(oauthService, userService) {
208209
@Override
@@ -215,7 +216,7 @@ boolean shouldTryCookieAuth(ApiMethodConfig config) {
215216
}
216217

217218
@Test
218-
public void testAuthenticateSkipTokenAuthCookieAuth() {
219+
public void testAuthenticateSkipTokenAuthCookieAuth() throws ServiceUnavailableException {
219220
attr.set(Attribute.SKIP_TOKEN_AUTH, true);
220221
authenticator = new GoogleAppEngineAuthenticator(oauthService, userService) {
221222
@Override
@@ -229,7 +230,7 @@ boolean shouldTryCookieAuth(ApiMethodConfig config) {
229230
}
230231

231232
@Test
232-
public void testAuthenticateOAuth2CookieAuthBothFail() {
233+
public void testAuthenticateOAuth2CookieAuthBothFail() throws ServiceUnavailableException {
233234
authenticator = new GoogleAppEngineAuthenticator(oauthService, userService) {
234235
@Override
235236
com.google.appengine.api.users.User getOAuth2User(HttpServletRequest request,
@@ -247,7 +248,7 @@ boolean shouldTryCookieAuth(ApiMethodConfig config) {
247248
}
248249

249250
@Test
250-
public void testAuthenticateOAuth2FailCookieAuth() {
251+
public void testAuthenticateOAuth2FailCookieAuth() throws ServiceUnavailableException {
251252
authenticator = new GoogleAppEngineAuthenticator(oauthService, userService) {
252253
@Override
253254
com.google.appengine.api.users.User getOAuth2User(HttpServletRequest request,

endpoints-framework/src/test/java/com/google/api/server/spi/auth/GoogleAuthTest.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
3232
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
3333
import com.google.api.server.spi.auth.GoogleAuth.TokenInfo;
34+
import com.google.api.server.spi.response.ServiceUnavailableException;
3435
import com.google.common.collect.ImmutableList;
3536

3637
import org.junit.Test;
@@ -166,7 +167,23 @@ public void testParseTokenInfo_withoutEmail() throws Exception {
166167
assertNull(GoogleAuth.parseTokenInfo(request));
167168
}
168169

170+
@Test
171+
public void testParseTokenInfo_with400() throws Exception {
172+
HttpRequest request = constructHttpRequest("{\"error_description\": \"Invalid Value\"}", 400);
173+
assertNull(GoogleAuth.parseTokenInfo(request));
174+
}
175+
176+
@Test(expected = ServiceUnavailableException.class)
177+
public void testParseTokenInfo_with500() throws Exception {
178+
HttpRequest request = constructHttpRequest("{\"error_description\": \"Backend Error\"}", 500);
179+
GoogleAuth.parseTokenInfo(request);
180+
}
181+
169182
private HttpRequest constructHttpRequest(final String content) throws IOException {
183+
return constructHttpRequest(content, 200);
184+
}
185+
186+
private HttpRequest constructHttpRequest(final String content, final int statusCode) throws IOException {
170187
HttpTransport transport = new MockHttpTransport() {
171188
@Override
172189
public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
@@ -176,12 +193,14 @@ public LowLevelHttpResponse execute() throws IOException {
176193
MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
177194
result.setContentType("application/json");
178195
result.setContent(content);
196+
result.setStatusCode(statusCode);
179197
return result;
180198
}
181199
};
182200
}
183201
};
184-
return transport.createRequestFactory().buildGetRequest(new GenericUrl("https://google.com"))
185-
.setParser(new JsonObjectParser(new JacksonFactory()));
202+
HttpRequest httpRequest = transport.createRequestFactory().buildGetRequest(new GenericUrl("https://google.com")).setParser(new JsonObjectParser(new JacksonFactory()));
203+
GoogleAuth.configureErrorHandling(httpRequest);
204+
return httpRequest;
186205
}
187206
}

0 commit comments

Comments
 (0)