Skip to content
This repository was archived by the owner on Jun 30, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.google.api.server.spi.config.Authenticator;
import com.google.api.server.spi.config.Singleton;
import com.google.api.server.spi.request.Attribute;
import com.google.api.server.spi.response.ServiceUnavailableException;
import com.google.common.annotations.VisibleForTesting;

import javax.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -51,7 +52,7 @@ public EndpointsAuthenticator(GoogleJwtAuthenticator jwtAuthenticator,
}

@Override
public User authenticate(HttpServletRequest request) {
public User authenticate(HttpServletRequest request) throws ServiceUnavailableException {
Attribute attr = Attribute.from(request);
User user = jwtAuthenticator.authenticate(request);
if (user == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.google.api.server.spi.config.model.ApiMethodConfig;
import com.google.api.server.spi.config.scope.AuthScopeExpression;
import com.google.api.server.spi.request.Attribute;
import com.google.api.server.spi.response.ServiceUnavailableException;
import com.google.appengine.api.oauth.OAuthRequestException;
import com.google.appengine.api.oauth.OAuthService;
import com.google.appengine.api.oauth.OAuthServiceFactory;
Expand Down Expand Up @@ -56,7 +57,7 @@ public GoogleAppEngineAuthenticator(OAuthService oauthService, UserService userS
}

@VisibleForTesting
String getOAuth2ClientIdDev(String token) {
String getOAuth2ClientIdDev(String token) throws ServiceUnavailableException {
GoogleAuth.TokenInfo tokenInfo = GoogleAuth.getTokenInfoRemote(token);
return tokenInfo != null ? tokenInfo.clientId : null;
}
Expand All @@ -68,7 +69,7 @@ boolean shouldTryCookieAuth(ApiMethodConfig config) {

@VisibleForTesting
com.google.appengine.api.users.User getOAuth2User(HttpServletRequest request,
ApiMethodConfig config) {
ApiMethodConfig config) throws ServiceUnavailableException {
String token = GoogleAuth.getAuthToken(request);
if (!GoogleAuth.isOAuth2Token(token)) {
return null;
Expand Down Expand Up @@ -114,7 +115,7 @@ com.google.appengine.api.users.User getOAuth2User(HttpServletRequest request,
}

@Override
public User authenticate(HttpServletRequest request) {
public User authenticate(HttpServletRequest request) throws ServiceUnavailableException {
Attribute attr = Attribute.from(request);
if (!EnvUtil.isRunningOnAppEngine()) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
package com.google.api.server.spi.auth;

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpIOExceptionHandler;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpUnsuccessfulResponseHandler;
import com.google.api.client.util.Key;
import com.google.api.server.spi.Client;
import com.google.api.server.spi.Constant;
import com.google.api.server.spi.Strings;
import com.google.api.server.spi.request.Attribute;
import com.google.api.server.spi.response.ServiceUnavailableException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;

Expand Down Expand Up @@ -172,29 +176,66 @@ public static class TokenInfo {
@Key("issued_to") public String clientId;
@Key("scope") public String scopes;
@Key("user_id") public String userId;
@Key("error_description") public String errorDescription;
}

/**
* Get OAuth2 token info from remote token validation API.
* Retries IOExceptions and 5xx responses once.
*/
static TokenInfo getTokenInfoRemote(String token) {
static TokenInfo getTokenInfoRemote(String token) throws ServiceUnavailableException {
try {
HttpRequest request = Client.getInstance().getJsonHttpRequestFactory()
.buildGetRequest(new GenericUrl(TOKEN_INFO_ENDPOINT + token));
configureErrorHandling(request);
return parseTokenInfo(request);
} catch (IOException e) {
logger.log(Level.WARNING, "Failed to retrieve tokeninfo", e);
return null;
throw new ServiceUnavailableException("Failed to perform access token validation", e);
}
}

@VisibleForTesting
static TokenInfo parseTokenInfo(HttpRequest request) throws IOException {
TokenInfo info = request.execute().parseAs(TokenInfo.class);
static TokenInfo parseTokenInfo(HttpRequest request)
throws IOException, ServiceUnavailableException {
HttpResponse response = request.execute();
int statusCode = response.getStatusCode();
TokenInfo info = response.parseAs(TokenInfo.class);
if (statusCode != 200) {
String errorDescription = "Unknown error";
if (info != null && info.errorDescription != null) {
errorDescription = info.errorDescription;
}
errorDescription += " (" + statusCode + ")";
if (statusCode >= 500) {
logger.log(Level.SEVERE, "Error validating access token: " + errorDescription);
throw new ServiceUnavailableException("Failed to validate access token");
}
logger.log(Level.INFO, "Invalid access token: " + errorDescription);
return null;
}
if (info == null || Strings.isEmptyOrWhitespace(info.email)) {
logger.log(Level.WARNING, "Access token does not contain email scope");
return null;
}
return info;
}

@VisibleForTesting
static void configureErrorHandling(HttpRequest request) {
request.setNumberOfRetries(1)
.setThrowExceptionOnExecuteError(false)
.setIOExceptionHandler(new HttpIOExceptionHandler() {
@Override
public boolean handleIOException(HttpRequest request, boolean supportsRetry) {
return true; // consider all IOException as transient
}
})
.setUnsuccessfulResponseHandler(new HttpUnsuccessfulResponseHandler() {
@Override
public boolean handleResponse(HttpRequest request, HttpResponse response,
boolean supportsRetry) {
return response.getStatusCode() >= 500; // only retry Google's backend errors
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.google.api.server.spi.config.Singleton;
import com.google.api.server.spi.config.model.ApiMethodConfig;
import com.google.api.server.spi.request.Attribute;
import com.google.api.server.spi.response.ServiceUnavailableException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;

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

@Override
public User authenticate(HttpServletRequest request) {
public User authenticate(HttpServletRequest request) throws ServiceUnavailableException {
Attribute attr = Attribute.from(request);
if (attr.isEnabled(Attribute.SKIP_TOKEN_AUTH)) {
return null;
Expand Down Expand Up @@ -89,7 +90,7 @@ public User authenticate(HttpServletRequest request) {
}

@VisibleForTesting
TokenInfo getTokenInfoRemote(String token) {
TokenInfo getTokenInfoRemote(String token) throws ServiceUnavailableException {
return GoogleAuth.getTokenInfoRemote(token);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.google.api.server.spi.EnvUtil;
import com.google.api.server.spi.auth.common.User;
import com.google.api.server.spi.request.Attribute;
import com.google.api.server.spi.response.ServiceUnavailableException;

import org.junit.Before;
import org.junit.Test;
Expand Down Expand Up @@ -53,13 +54,13 @@ public void setUp() throws Exception {
}

@Test
public void testAuthenticate_jwt() {
public void testAuthenticate_jwt() throws ServiceUnavailableException {
when(jwtAuthenticator.authenticate(request)).thenReturn(USER);
assertEquals(USER, authenticator.authenticate(request));
}

@Test
public void testAuthenticate_appEngine() {
public void testAuthenticate_appEngine() throws ServiceUnavailableException {
when(jwtAuthenticator.authenticate(request)).thenReturn(null);
when(appEngineAuthenticator.authenticate(request)).thenReturn(USER);

Expand All @@ -70,7 +71,7 @@ public void testAuthenticate_appEngine() {
}

@Test
public void testAuthenticate_oauth2NonAppEngine() {
public void testAuthenticate_oauth2NonAppEngine() throws ServiceUnavailableException {
when(jwtAuthenticator.authenticate(request)).thenReturn(null);
when(oauth2Authenticator.authenticate(request)).thenReturn(USER);

Expand All @@ -81,7 +82,7 @@ public void testAuthenticate_oauth2NonAppEngine() {
}

@Test
public void testAuthenticate_oAuth2NotRequireAppEngineUser() {
public void testAuthenticate_oAuth2NotRequireAppEngineUser() throws ServiceUnavailableException {
when(jwtAuthenticator.authenticate(request)).thenReturn(null);
when(oauth2Authenticator.authenticate(request)).thenReturn(USER);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.google.api.server.spi.config.model.ApiMethodConfig;
import com.google.api.server.spi.config.scope.AuthScopeExpressions;
import com.google.api.server.spi.request.Attribute;
import com.google.api.server.spi.response.ServiceUnavailableException;
import com.google.appengine.api.oauth.OAuthRequestException;
import com.google.appengine.api.oauth.OAuthService;
import com.google.appengine.api.users.UserService;
Expand Down Expand Up @@ -87,7 +88,7 @@ private void initializeRequest(String bearerString) {
}

@Test
public void testGetOAuth2UserNonOAuth2() {
public void testGetOAuth2UserNonOAuth2() throws ServiceUnavailableException {
initializeRequest("Bearer badToken");
assertNull(authenticator.getOAuth2User(request, config));

Expand Down Expand Up @@ -152,27 +153,27 @@ public void testGetOAuth2UserSkipClientIdCheck() throws Exception {
}

@Test
public void testGetOAuth2UserAppEngineDevClientIdNotAllowed() {
public void testGetOAuth2UserAppEngineDevClientIdNotAllowed() throws ServiceUnavailableException {
System.setProperty(EnvUtil.ENV_APPENGINE_RUNTIME, "Developement");
when(config.getScopeExpression()).thenReturn(AuthScopeExpressions.interpret(SCOPES));
when(config.getClientIds()).thenReturn(ImmutableList.of("clientId2"));
assertNull(authenticator.getOAuth2User(request, config));
}

@Test
public void testAuthenticateNonAppEngine() {
public void testAuthenticateNonAppEngine() throws ServiceUnavailableException {
System.clearProperty(EnvUtil.ENV_APPENGINE_RUNTIME);
assertNull(authenticator.authenticate(request));
}

@Test
public void testAuthenticateSkipTokenAuth() {
public void testAuthenticateSkipTokenAuth() throws ServiceUnavailableException {
attr.set(Attribute.SKIP_TOKEN_AUTH, true);
assertNull(authenticator.authenticate(request));
}

@Test
public void testAuthenticateOAuth2Fail() {
public void testAuthenticateOAuth2Fail() throws ServiceUnavailableException {
authenticator = new GoogleAppEngineAuthenticator(oauthService, userService) {
@Override
com.google.appengine.api.users.User getOAuth2User(HttpServletRequest request,
Expand All @@ -189,7 +190,7 @@ boolean shouldTryCookieAuth(ApiMethodConfig config) {
}

@Test
public void testAuthenticateOAuth2() {
public void testAuthenticateOAuth2() throws ServiceUnavailableException {
authenticator = new GoogleAppEngineAuthenticator(oauthService, userService) {
@Override
com.google.appengine.api.users.User getOAuth2User(HttpServletRequest request,
Expand All @@ -202,7 +203,7 @@ com.google.appengine.api.users.User getOAuth2User(HttpServletRequest request,
}

@Test
public void testAuthenticateSkipTokenAuthCookieAuthFail() {
public void testAuthenticateSkipTokenAuthCookieAuthFail() throws ServiceUnavailableException {
attr.set(Attribute.SKIP_TOKEN_AUTH, true);
authenticator = new GoogleAppEngineAuthenticator(oauthService, userService) {
@Override
Expand All @@ -215,7 +216,7 @@ boolean shouldTryCookieAuth(ApiMethodConfig config) {
}

@Test
public void testAuthenticateSkipTokenAuthCookieAuth() {
public void testAuthenticateSkipTokenAuthCookieAuth() throws ServiceUnavailableException {
attr.set(Attribute.SKIP_TOKEN_AUTH, true);
authenticator = new GoogleAppEngineAuthenticator(oauthService, userService) {
@Override
Expand All @@ -229,7 +230,7 @@ boolean shouldTryCookieAuth(ApiMethodConfig config) {
}

@Test
public void testAuthenticateOAuth2CookieAuthBothFail() {
public void testAuthenticateOAuth2CookieAuthBothFail() throws ServiceUnavailableException {
authenticator = new GoogleAppEngineAuthenticator(oauthService, userService) {
@Override
com.google.appengine.api.users.User getOAuth2User(HttpServletRequest request,
Expand All @@ -247,7 +248,7 @@ boolean shouldTryCookieAuth(ApiMethodConfig config) {
}

@Test
public void testAuthenticateOAuth2FailCookieAuth() {
public void testAuthenticateOAuth2FailCookieAuth() throws ServiceUnavailableException {
authenticator = new GoogleAppEngineAuthenticator(oauthService, userService) {
@Override
com.google.appengine.api.users.User getOAuth2User(HttpServletRequest request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
import com.google.api.server.spi.auth.GoogleAuth.TokenInfo;
import com.google.api.server.spi.response.ServiceUnavailableException;
import com.google.common.collect.ImmutableList;

import org.junit.Test;
Expand Down Expand Up @@ -166,7 +167,23 @@ public void testParseTokenInfo_withoutEmail() throws Exception {
assertNull(GoogleAuth.parseTokenInfo(request));
}

@Test
public void testParseTokenInfo_with400() throws Exception {
HttpRequest request = constructHttpRequest("{\"error_description\": \"Invalid Value\"}", 400);
assertNull(GoogleAuth.parseTokenInfo(request));
}

@Test(expected = ServiceUnavailableException.class)
public void testParseTokenInfo_with500() throws Exception {
HttpRequest request = constructHttpRequest("{\"error_description\": \"Backend Error\"}", 500);
GoogleAuth.parseTokenInfo(request);
}

private HttpRequest constructHttpRequest(final String content) throws IOException {
return constructHttpRequest(content, 200);
}

private HttpRequest constructHttpRequest(final String content, final int statusCode) throws IOException {
HttpTransport transport = new MockHttpTransport() {
@Override
public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
Expand All @@ -176,12 +193,14 @@ public LowLevelHttpResponse execute() throws IOException {
MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
result.setContentType("application/json");
result.setContent(content);
result.setStatusCode(statusCode);
return result;
}
};
}
};
return transport.createRequestFactory().buildGetRequest(new GenericUrl("https://google.com"))
.setParser(new JsonObjectParser(new JacksonFactory()));
HttpRequest httpRequest = transport.createRequestFactory().buildGetRequest(new GenericUrl("https://google.com")).setParser(new JsonObjectParser(new JacksonFactory()));
GoogleAuth.configureErrorHandling(httpRequest);
return httpRequest;
}
}
Loading