Reputation:
I have created simple app using Google Drive API with OAuth2 authentication based on this sample plus-appengine-sample
So, I have two servlets implementations: AbstractAppEngineAuthorizationCodeServlet
and AbstractAppEngineAuthorizationCodeCallbackServlet
which should do all the hard work for me (oauth work flow).
public class DriveServlet extends AbstractAppEngineAuthorizationCodeServlet {
private static final String MY_APP_NAME = "Drive API demo";
private static final long serialVersionUID = 1L;
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
AuthorizationCodeFlow authFlow = initializeFlow();
Credential credential = authFlow.loadCredential(getUserId(req));
if (credential == null) {
resp.sendRedirect(authFlow.newAuthorizationUrl()
.setRedirectUri(OAuthUtils.getRedirectUri(req)).build());
return;
}
Drive drive = new Drive.Builder(OAuthUtils.HTTP_TRANSPORT_REQUEST,
OAuthUtils.JSON_FACTORY, credential).setApplicationName(MY_APP_NAME).build();
// API calls (examines drive structure)
DriveMiner miner = new DriveMiner(drive);
req.setAttribute("miner", miner);
RequestDispatcher view = req.getRequestDispatcher("/Drive.jsp");
view.forward(req, resp);
}
@Override
protected AuthorizationCodeFlow initializeFlow() throws ServletException, IOException {
return OAuthUtils.initializeFlow();
}
@Override
protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
return OAuthUtils.getRedirectUri(req);
}
}
public class OAuthCallbackServlet extends AbstractAppEngineAuthorizationCodeCallbackServlet {
private static final long serialVersionUID = 1L;
@Override
protected AuthorizationCodeFlow initializeFlow() throws ServletException, IOException {
return OAuthUtils.initializeFlow();
}
@Override
protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
return OAuthUtils.getRedirectUri(req);
}
@Override
protected void onSuccess(HttpServletRequest req, HttpServletResponse resp,
Credential credential) throws ServletException, IOException {
resp.sendRedirect(OAuthUtils.MAIN_SERVLET_PATH);
}
@Override
protected void onError(HttpServletRequest req, HttpServletResponse resp,
AuthorizationCodeResponseUrl errorResponse) throws ServletException, IOException {
String nickname = UserServiceFactory.getUserService().getCurrentUser().getNickname();
resp.getWriter().print(
"<h3>I am sorry" + nickname+ ", an internal server error occured. Try it later.</h1>");
resp.setStatus(500);
resp.addHeader("Content-Type", "text/html");
return;
}
}
public class OAuthUtils {
private static final String CLIENT_SECRETS_FILE_PATH = "/client_secrets.json";
static final JacksonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
static final UrlFetchTransport HTTP_TRANSPORT_REQUEST = new UrlFetchTransport();
private static final Set<String> PERMISSION_SCOPES = Collections.singleton(DriveScopes.DRIVE_READONLY);
private static final AppEngineDataStoreFactory DATA_STORE_FACTORY = AppEngineDataStoreFactory.getDefaultInstance();
private static final String AUTH_CALLBACK_SERVLET_PATH = "/oauth2callback";
static final String MAIN_SERVLET_PATH = "/drive";
private static GoogleClientSecrets clientSecrets = null;
private OAuthUtils() {}
private static GoogleClientSecrets getClientSecrets() throws IOException {
if (clientSecrets == null) {
InputStream jsonStream = OAuthUtils.class.getResourceAsStream(CLIENT_SECRETS_FILE_PATH);
InputStreamReader jsonReader = new InputStreamReader(jsonStream);
clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, jsonReader);
}
return clientSecrets;
}
static GoogleAuthorizationCodeFlow initializeFlow() throws IOException {
return new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT_REQUEST,
JSON_FACTORY, getClientSecrets(), PERMISSION_SCOPES)
.setDataStoreFactory(DATA_STORE_FACTORY)
.setAccessType("offline").build();
}
static String getRedirectUri(HttpServletRequest req) {
GenericUrl requestUrl = new GenericUrl(req.getRequestURL().toString());
requestUrl.setRawPath(AUTH_CALLBACK_SERVLET_PATH);
return requestUrl.build();
}
}
Authentication flow works as expected as well as Drive API calls, but somehow, after some period of time, I'm getting this exception on refresh:
Uncaught exception from servlet
com.google.api.client.googleapis.json.GoogleJsonResponseException: 401
{
"code" : 401,
"errors" : [{ "domain" : "global",
"location" : "Authorization",
"locationType" : "header",
"message" : "Invalid Credentials",
"reason" : "authError" }],
"message" : "Invalid Credentials"
}
at com.google.api.client.googleapis.json.GoogleJsonResponseException.from(GoogleJsonResponseException.java:145)
at com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest.newExceptionOnError(AbstractGoogleJsonClientRequest.java:113)
at com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest.newExceptionOnError(AbstractGoogleJsonClientRequest.java:40)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest$1.interceptResponse(AbstractGoogleClientRequest.java:312)
at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:1049)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:410)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:343)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.execute(AbstractGoogleClientRequest.java:460)
at sk.ennova.teamscom.drive.DriveMiner.getRootFolderId(DriveMiner.java:46)
at org.apache.jsp.Drive_jsp._jspService(Drive_jsp.java:61)
It seems that token has expired, but isn't it a work for servlets to request a new access token with the refresh token which they stored? I use offline access type, so refresh token should be delivered to callback servlet at first request.
Here "401 Unauthorized" when trying to watch changes on Google Drive with Java API Client are some hints where could be the problem, but handling token expiration should not be my case if I'm using these servlets (correct me if I am wrong). Also scope DriveScopes.DRIVE_READONLY
seems OK for reading "drive" tree structure (get files of given folder and so on). Where could be the problem?
Upvotes: 0
Views: 1052
Reputation: 2703
You need to first specify that you need a refresh token for offline / long term access and then save the refresh token for later use when the access token expires. You can request a new access token using the refresh token until the user revokes your access to her account. See the official documentation here:
https://developers.google.com/accounts/docs/OAuth2WebServer#refresh
Upvotes: 1