0

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?

Community
  • 1
  • 1
matoni
  • 2,479
  • 21
  • 39

1 Answers1

1

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

Price
  • 2,683
  • 3
  • 17
  • 43
  • I have access type offline in `OAuthUtils#initializeFlow()`. I've tried to call `credential.refreshToken()`, but it's still not working. I made some info prints - if I come back to my app, lets say after 2 hours, it will show old token with negative expiration. After page refresh it will show new expiration even without explicit call `credential.refreshToken()`, so servlets made token exchange automatically on background. But why after explicit page refresh? – matoni Jan 30 '15 at 22:00
  • Are you not saving the token received during the first page load in a persistent storage and getting a new token each time the servlet is invoked? – Price Jan 31 '15 at 06:40
  • 1
    No, I don't have to store tokens explicitly. `AbstractAuthorizationCodeServlet`, which is a superclass of `DriveServlet`, stores them. According to it's javadoc, I just need to call `loadCredential` to get persistent credential with tokens. Anyway, **I wrongly assumed that used abstract servlets automatically refreshes access token when necessecary** (they only do auth flow and stores the result - tokens). Calling `credential.refreshToken()`, **as you recommended**, is the way to do that. The reason why it didn't work the first time was, that I was testing wrong app version :). – matoni Feb 01 '15 at 02:32