package org.fluxtream.connectors.controllers; import net.sf.json.JSONObject; import org.codehaus.plexus.util.ExceptionUtils; import org.fluxtream.core.Configuration; import org.fluxtream.core.aspects.FlxLogger; import org.fluxtream.core.auth.AuthHelper; import org.fluxtream.core.connectors.Connector; import org.fluxtream.core.connectors.OAuth2Helper; import org.fluxtream.core.domain.ApiKey; import org.fluxtream.core.domain.Guest; import org.fluxtream.core.domain.Notification; import org.fluxtream.core.services.ConnectorUpdateService; import org.fluxtream.core.services.GuestService; import org.fluxtream.core.services.NotificationsService; import org.fluxtream.core.services.SystemService; import org.fluxtream.core.utils.HttpUtils; import org.fluxtream.core.utils.UnexpectedHttpResponseCodeException; import org.fluxtream.mvc.controllers.ErrorController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; @Controller @RequestMapping(value = "/google/oauth2") public class GoogleOAuth2Controller { FlxLogger logger = FlxLogger.getLogger(GoogleOAuth2Controller.class); @Autowired Configuration env; @Autowired SystemService systemService; @Autowired GuestService guestService; @Autowired NotificationsService notificationsService; @Autowired ConnectorUpdateService connectorUpdateService; @Autowired OAuth2Helper oAuth2Helper; @Autowired ErrorController errorController; private final static String APIKEYID_ATTRIBUTE = "google.oauth2.apiKeyId"; @RequestMapping(value = "/{apiKeyId}/token") public String renewToken(@PathVariable("apiKeyId") String apiKeyId, HttpServletRequest request) throws IOException, ServletException { request.getSession().setAttribute(APIKEYID_ATTRIBUTE, apiKeyId); final ApiKey apiKey = guestService.getApiKey(Long.valueOf(apiKeyId)); // Check if the stored client ID matches the one in our properties file, and only in that // case try to revoke. TODO: in that case skip next two lines String propertiesClientId = env.get("google.client.id"); String storedClientId = guestService.getApiKeyAttribute(apiKey,"google.client.id"); if(propertiesClientId !=null && storedClientId!=null && propertiesClientId.equals(storedClientId)) { // Renewing token for the same server, do revoke attempt first final String refreshTokenRemoveURL = guestService.getApiKeyAttribute(apiKey,"refreshTokenRemoveURL"); oAuth2Helper.revokeRefreshToken(apiKey.getGuestId(), apiKey.getConnector(), refreshTokenRemoveURL); String msg="Called revokeRefreshToken with refreshTokenRemoveURL='" + refreshTokenRemoveURL + "'"; StringBuilder sb = new StringBuilder("module=GoogleOauth2Controller component=renewToken action=renewToken apiKeyId=" + apiKeyId) .append(" guestId=").append(apiKey.getGuestId()) .append(" message=\"").append(msg).append("\""); logger.info(sb.toString()); } else { String msg="Skipped revokeRefreshToken, stored and properties google.client.id do not match (" + storedClientId + " vs " + propertiesClientId + ")" ; StringBuilder sb = new StringBuilder("module=GoogleOauth2Controller component=renewToken action=renewToken apiKeyId=" + apiKeyId) .append(" guestId=").append(apiKey.getGuestId()) .append(" message=\"").append(msg).append("\""); logger.info(sb.toString()); } // Continue on to ask user for authorization whether or not you did a revoke return getToken(request); } @RequestMapping(value = "/token") public String getToken(HttpServletRequest request) throws IOException, ServletException{ String scope = request.getParameter("scope"); request.getSession().setAttribute("oauth2Scope", scope); String redirectUri = ControllerSupport.getLocationBase(request, env) + "google/oauth2/swapToken"; String clientId = env.get("google.client.id"); String authorizeUrl = "https://accounts.google.com/o/oauth2/auth?client_id=" + clientId + "&redirect_uri=" + redirectUri + "&scope=" + scope + "&response_type=code&" + "access_type=offline&" + "approval_prompt=force"; return "redirect:" + authorizeUrl; } @RequestMapping(value = "/swapToken") public ModelAndView upgradeToken(HttpServletRequest request) throws IOException, UnexpectedHttpResponseCodeException { String scope = (String) request.getSession().getAttribute("oauth2Scope"); Connector scopedApi = systemService.getApiFromGoogleScope(scope); Guest guest = AuthHelper.getGuest(); String errorParameter = request.getParameter("error"); if (errorParameter!=null) { notificationsService.addNamedNotification(guest.getId(), Notification.Type.WARNING, scopedApi.statusNotificationName(), "There was a problem importing your " + scopedApi.prettyName() + " data: " + errorParameter); return new ModelAndView("redirect:/app/"); } String swapTokenUrl = "https://accounts.google.com/o/oauth2/token"; String code = request.getParameter("code"); String redirectUri = ControllerSupport.getLocationBase(request, env) + "google/oauth2/swapToken"; Map<String,String> params = new HashMap<String,String>(); params.put("code", code); params.put("client_id", env.get("google.client.id")); params.put("client_secret", env.get("google.client.secret")); params.put("redirect_uri", redirectUri); params.put("grant_type", "authorization_code"); // Get the google branding info. Default to fluxtream if not set, but can override in // oauth.properties by setting the default google.client.brandName parameter String brandName = env.get("google.client.brandName"); if(brandName == null) { // Not set in oauth.properties file, default to "Fluxtream" brandName="Fluxtream"; } // Try to renew the token. On failure leave token=null JSONObject token = null; try { String fetched = HttpUtils.fetch(swapTokenUrl, params); token = JSONObject.fromObject(fetched); } catch(Throwable e) { token = null; } ApiKey apiKey; final boolean isRenewToken = request.getSession().getAttribute(APIKEYID_ATTRIBUTE) != null; if (isRenewToken) { String apiKeyId = (String)request.getSession().getAttribute(APIKEYID_ATTRIBUTE); apiKey = guestService.getApiKey(Long.valueOf(apiKeyId)); if (apiKey==null) { Exception e = new Exception(); String stackTrace = ExceptionUtils.getStackTrace(e); String errorMessage = "no apiKey with id '%s'... It looks like you are trying to renew the tokens of a non-existing Connector (/ApiKey)"; return errorController.handleError(500, errorMessage, stackTrace); } if (token == null || !token.has("refresh_token")) { String message = (new StringBuilder("<p>We couldn't get your oauth2 refresh token. ")) .append("Something went wrong.</p>") .append("<p>You'll have to surf to your ") .append("<a target='_new' href='https://accounts.google.com/b/0/IssuedAuthSubTokens'>token mgmt page at Google</a> ") .append("and hit \"Revoke Access\" next to \"").append(brandName).append(" — ").append(getGooglePrettyName(scopedApi)).append("\"</p>") .append("<p>Then please, head to <a href=\"javascript:App.manageConnectors()\">Manage Connectors</a> ") .append("and renew your tokens (look for the <i class=\"icon-resize-small icon-large\"></i> icon)</p>") .append("<p>We apologize for the inconvenience</p>").toString(); notificationsService.addNamedNotification(guest.getId(), Notification.Type.ERROR, apiKey.getConnector().statusNotificationName(), message); // Record permanent failure since this connector won't work again until // it is reauthenticated guestService.setApiKeyStatus(apiKey.getId(), ApiKey.Status.STATUS_PERMANENT_FAILURE, null, ApiKey.PermanentFailReason.NEEDS_REAUTH); return new ModelAndView("redirect:/app"); } // Remove oauth1 keys if upgrading from previous connector version. // Remember whether or not we're upgrading from previous connector version. // If so, do a full history update. Otherwise don't force a full // history update and allow the update to be whatever it normally would be boolean upgradeFromOauth1 = false; if (guestService.getApiKeyAttribute(apiKey, "googleConsumerKey")!=null) { guestService.removeApiKeyAttribute(apiKey.getId(), "googleConsumerKey"); upgradeFromOauth1 = true; } if (guestService.getApiKeyAttribute(apiKey, "googleConsumerSecret")!=null) { guestService.removeApiKeyAttribute(apiKey.getId(), "googleConsumerSecret"); upgradeFromOauth1 = true; } // If upgradeFromOauth1 reset the connector to force a full reimport on google calendar, // otherwise just do a normal update if (apiKey.getConnector().getName().equals("google_calendar")) { connectorUpdateService.flushUpdateWorkerTasks(apiKey, upgradeFromOauth1); } } else { apiKey = guestService.createApiKey(guest.getId(), scopedApi); } // We need to store google.client.id and google.client.secret with the // apiKeyAttributes in either the case of original creation of the key // or token renewal. createApiKey actually handles the former case, but // not the latter. Do it in all cases here. guestService.setApiKeyAttribute(apiKey, "google.client.id", env.get("google.client.id")); guestService.setApiKeyAttribute(apiKey, "google.client.secret", env.get("google.client.secret")); final String refresh_token = token.getString("refresh_token"); guestService.setApiKeyAttribute(apiKey, "accessToken", token.getString("access_token")); guestService.setApiKeyAttribute(apiKey, "tokenExpires", String.valueOf(System.currentTimeMillis() + (token.getLong("expires_in")*1000))); guestService.setApiKeyAttribute(apiKey, "refreshToken", refresh_token); final String encodedRefreshToken = URLEncoder.encode(refresh_token, "UTF-8"); guestService.setApiKeyAttribute(apiKey, "refreshTokenRemoveURL", "https://accounts.google.com/o/oauth2/revoke?token=" + encodedRefreshToken); // Record this connector as having status up guestService.setApiKeyStatus(apiKey.getId(), ApiKey.Status.STATUS_UP, null, null); // Schedule an update for this connector connectorUpdateService.updateConnector(apiKey, false); if (isRenewToken) { request.getSession().removeAttribute(APIKEYID_ATTRIBUTE); return new ModelAndView("redirect:/app/tokenRenewed/"+scopedApi.getName()); } return new ModelAndView("redirect:/app/from/"+scopedApi.getName()); } private Object getGooglePrettyName(final Connector scopedApi) { if (scopedApi.getName().equals("google_latitude")) return "Latitude"; else if (scopedApi.getName().equals("google_calendar")) return "Google Calendar (or Google Agenda)"; else { logger.warn("Please check Google's pretty name for API " + scopedApi.getName() + " (see GoogleOAuth2Controller.java)"); return scopedApi.prettyName(); } } }