/* * JBoss, a division of Red Hat * Copyright 2013, Red Hat Middleware, LLC, and individual * contributors as indicated by the @authors tag. See the * copyright.txt in the distribution for a full listing of * individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.gatein.security.oauth.google; import java.io.IOException; import java.util.HashSet; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeRequestUrl; import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.googleapis.auth.oauth2.GoogleRefreshTokenRequest; import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.jackson.JacksonFactory; import com.google.api.services.oauth2.Oauth2; import com.google.api.services.oauth2.model.Tokeninfo; import com.google.api.services.oauth2.model.Userinfo; import com.google.api.services.plus.Plus; import org.exoplatform.container.ExoContainerContext; import org.exoplatform.container.xml.InitParams; import org.exoplatform.container.xml.ValueParam; import org.exoplatform.services.organization.UserProfile; import org.exoplatform.web.security.security.SecureRandomService; import org.gatein.security.oauth.spi.InteractionState; import org.gatein.security.oauth.exception.OAuthException; import org.gatein.security.oauth.exception.OAuthExceptionCode; import org.gatein.common.logging.Logger; import org.gatein.common.logging.LoggerFactory; import org.gatein.security.oauth.spi.OAuthCodec; import org.gatein.security.oauth.common.OAuthConstants; import org.gatein.security.oauth.utils.OAuthPersistenceUtils; /** * {@inheritDoc} * * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> */ public class GoogleProcessorImpl implements GoogleProcessor { private static Logger log = LoggerFactory.getLogger(GoogleProcessorImpl.class); private final String redirectURL; private final String clientID; private final String clientSecret; private final Set<String> scopes = new HashSet<String>(); private final String accessType; private final String applicationName; private final int chunkLength; private final SecureRandomService secureRandomService; /** * Default HTTP transport to use to make HTTP requests. */ private final HttpTransport TRANSPORT = new NetHttpTransport(); /** * Default JSON factory to use to deserialize JSON. */ private final JacksonFactory JSON_FACTORY = new JacksonFactory(); public GoogleProcessorImpl(ExoContainerContext context, InitParams params, SecureRandomService secureRandomService) { this.clientID = params.getValueParam("clientId").getValue(); this.clientSecret = params.getValueParam("clientSecret").getValue(); String redirectURLParam = params.getValueParam("redirectURL").getValue(); String scope = params.getValueParam("scope").getValue(); this.accessType = params.getValueParam("accessType").getValue(); ValueParam appNameParam = params.getValueParam("applicationName"); if (appNameParam != null && appNameParam.getValue() != null) { applicationName = appNameParam.getValue(); } else { applicationName = "GateIn portal"; } if (clientID == null || clientID.length() == 0 || clientID.trim().equals("<<to be replaced>>")) { throw new IllegalArgumentException("Property 'clientId' needs to be provided. The value should be " + "clientId of your Google application"); } if (clientSecret == null || clientSecret.length() == 0 || clientSecret.trim().equals("<<to be replaced>>")) { throw new IllegalArgumentException("Property 'clientSecret' needs to be provided. The value should be " + "clientSecret of your Google application"); } if (redirectURLParam == null || redirectURLParam.length() == 0) { this.redirectURL = "http://localhost:8080/" + context.getName() + OAuthConstants.GOOGLE_AUTHENTICATION_URL_PATH; } else { this.redirectURL = redirectURLParam.replaceAll("@@portal.container.name@@", context.getName()); } addScopesFromString(scope, this.scopes); this.chunkLength = OAuthPersistenceUtils.getChunkLength(params); if (log.isDebugEnabled()) { log.debug("configuration: clientId=" + clientID + ", clientSecret=" + clientSecret + ", redirectURL=" + redirectURL + ", scope=" + scopes + ", accessType=" + accessType + ", applicationName=" + applicationName + ", chunkLength=" + chunkLength); } this.secureRandomService = secureRandomService; } @Override public InteractionState<GoogleAccessTokenContext> processOAuthInteraction(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, OAuthException { return processOAuthInteractionImpl(httpRequest, httpResponse, this.scopes); } @Override public InteractionState<GoogleAccessTokenContext> processOAuthInteraction(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String scope) throws IOException, OAuthException { Set<String> scopes = new HashSet<String>(); addScopesFromString(scope, scopes); return processOAuthInteractionImpl(httpRequest, httpResponse, scopes); } protected InteractionState<GoogleAccessTokenContext> processOAuthInteractionImpl(HttpServletRequest request, HttpServletResponse response, Set<String> scopes) throws IOException { HttpSession session = request.getSession(); String state = (String) session.getAttribute(OAuthConstants.ATTRIBUTE_AUTH_STATE); // Very initial request to portal if (state == null || state.isEmpty()) { return initialInteraction(request, response, scopes); } else if (state.equals(InteractionState.State.AUTH.name())) { GoogleTokenResponse tokenResponse = obtainAccessToken(request); GoogleAccessTokenContext accessTokenContext = validateTokenAndUpdateScopes(new GoogleAccessTokenContext(tokenResponse)); // Clear session attributes session.removeAttribute(OAuthConstants.ATTRIBUTE_AUTH_STATE); session.removeAttribute(OAuthConstants.ATTRIBUTE_VERIFICATION_STATE); return new InteractionState<GoogleAccessTokenContext>(InteractionState.State.FINISH, accessTokenContext); } // Likely shouldn't happen... return new InteractionState<GoogleAccessTokenContext>(InteractionState.State.valueOf(state), null); } protected InteractionState<GoogleAccessTokenContext> initialInteraction(HttpServletRequest request, HttpServletResponse response, Set<String> scopes) throws IOException { String verificationState = String.valueOf(secureRandomService.getSecureRandom().nextLong()); String authorizeUrl = new GoogleAuthorizationCodeRequestUrl(clientID, redirectURL, scopes). setState(verificationState).setAccessType(accessType).build(); if (log.isTraceEnabled()) { log.trace("Starting OAuth2 interaction with Google+"); log.trace("URL to send to Google+: " + authorizeUrl); } HttpSession session = request.getSession(); session.setAttribute(OAuthConstants.ATTRIBUTE_VERIFICATION_STATE, verificationState); session.setAttribute(OAuthConstants.ATTRIBUTE_AUTH_STATE, InteractionState.State.AUTH.name()); response.sendRedirect(authorizeUrl); return new InteractionState<GoogleAccessTokenContext>(InteractionState.State.AUTH, null); } protected GoogleTokenResponse obtainAccessToken(HttpServletRequest request) throws IOException { HttpSession session = request.getSession(); String stateFromSession = (String)session.getAttribute(OAuthConstants.ATTRIBUTE_VERIFICATION_STATE); String stateFromRequest = request.getParameter(OAuthConstants.STATE_PARAMETER); if (stateFromSession == null || stateFromRequest == null || !stateFromSession.equals(stateFromRequest)) { throw new OAuthException(OAuthExceptionCode.INVALID_STATE, "Validation of state parameter failed. stateFromSession=" + stateFromSession + ", stateFromRequest=" + stateFromRequest); } // Check if user didn't permit scope String error = request.getParameter(OAuthConstants.ERROR_PARAMETER); if (error != null) { if (OAuthConstants.ERROR_ACCESS_DENIED.equals(error)) { throw new OAuthException(OAuthExceptionCode.USER_DENIED_SCOPE, error); } else { throw new OAuthException(OAuthExceptionCode.UNKNOWN_ERROR, error); } } else { String code = request.getParameter(OAuthConstants.CODE_PARAMETER); GoogleTokenResponse tokenResponse = new GoogleAuthorizationCodeTokenRequest(TRANSPORT, JSON_FACTORY, clientID, clientSecret, code, redirectURL).execute(); if (log.isTraceEnabled()) { log.trace("Successfully obtained accessToken from google: " + tokenResponse); } return tokenResponse; } } @Override public GoogleAccessTokenContext validateTokenAndUpdateScopes(GoogleAccessTokenContext accessTokenContext) { GoogleRequest<Tokeninfo> googleRequest = new GoogleRequest<Tokeninfo>() { @Override protected Tokeninfo invokeRequest(GoogleAccessTokenContext accessTokenContext) throws IOException { GoogleTokenResponse tokenData = accessTokenContext.getTokenData(); Oauth2 oauth2 = getOAuth2InstanceImpl(tokenData); GoogleCredential credential = getGoogleCredential(tokenData); return oauth2.tokeninfo().setAccessToken(credential.getAccessToken()).execute(); } @Override protected OAuthException createException(IOException cause) { if (cause instanceof HttpResponseException) { return new OAuthException(OAuthExceptionCode.ACCESS_TOKEN_ERROR, "Error when obtaining tokenInfo: " + cause.getMessage(), cause); } else { return new OAuthException(OAuthExceptionCode.IO_ERROR, "IO Error when obtaining tokenInfo: " + cause.getMessage(), cause); } } }; Tokeninfo tokenInfo = googleRequest.executeRequest(accessTokenContext, this); // If there was an error in the token info, abort. if (tokenInfo.containsKey("error")) { throw new OAuthException(OAuthExceptionCode.ACCESS_TOKEN_ERROR, "Error during token validation: " + tokenInfo.get("error").toString()); } if (!tokenInfo.getIssuedTo().equals(clientID)) { throw new OAuthException(OAuthExceptionCode.ACCESS_TOKEN_ERROR, "Token's client ID does not match app's. clientID from tokenINFO: " + tokenInfo.getIssuedTo()); } if (log.isTraceEnabled()) { log.trace("Successfully validated accessToken from google: " + tokenInfo); } String[] scopes = tokenInfo.getScope().split(" "); return new GoogleAccessTokenContext(accessTokenContext.getTokenData(), scopes); } @Override public <C> C getAuthorizedSocialApiObject(GoogleAccessTokenContext accessToken, Class<C> socialApiObjectType) { if (Oauth2.class.equals(socialApiObjectType)) { return socialApiObjectType.cast(getOAuth2Instance(accessToken)); } else if (Plus.class.equals(socialApiObjectType)) { return socialApiObjectType.cast(getPlusService(accessToken)); } else { if (log.isDebugEnabled()) { log.debug("Class '" + socialApiObjectType + "' not supported by this processor"); } return null; } } @Override public Userinfo obtainUserInfo(GoogleAccessTokenContext accessTokenContext) { final Oauth2 oauth2 = getOAuth2Instance(accessTokenContext); GoogleRequest<Userinfo> googleRequest = new GoogleRequest<Userinfo>() { @Override protected Userinfo invokeRequest(GoogleAccessTokenContext accessTokenContext) throws IOException { return oauth2.userinfo().v2().me().get().execute(); } @Override protected OAuthException createException(IOException cause) { if (cause instanceof HttpResponseException) { return new OAuthException(OAuthExceptionCode.ACCESS_TOKEN_ERROR, "Error when obtaining userInfo: " + cause.getMessage(), cause); } else { return new OAuthException(OAuthExceptionCode.IO_ERROR, "IO Error when obtaining userInfo: " + cause.getMessage(), cause); } } }; Userinfo uinfo = googleRequest.executeRequest(accessTokenContext, this); if (log.isTraceEnabled()) { log.trace("Successfully obtained userInfo from google: " + uinfo); } return uinfo; } @Override public Oauth2 getOAuth2Instance(GoogleAccessTokenContext accessTokenContext) { GoogleTokenResponse tokenData = accessTokenContext.getTokenData(); return getOAuth2InstanceImpl(tokenData); } protected Oauth2 getOAuth2InstanceImpl(GoogleTokenResponse tokenData) { GoogleCredential credential = getGoogleCredential(tokenData); return new Oauth2.Builder(TRANSPORT, JSON_FACTORY, credential).setApplicationName(applicationName).build(); } @Override public Plus getPlusService(GoogleAccessTokenContext accessTokenContext) { GoogleTokenResponse tokenData = accessTokenContext.getTokenData(); // Build credential from stored token data. GoogleCredential credential = getGoogleCredential(tokenData); // Create a new authorized API client. Plus service = new Plus.Builder(TRANSPORT, JSON_FACTORY, credential) .setApplicationName(applicationName) .build(); return service; } private GoogleCredential getGoogleCredential(GoogleTokenResponse tokenResponse) { return new GoogleCredential.Builder() .setJsonFactory(JSON_FACTORY) .setTransport(TRANSPORT) .setClientSecrets(clientID, clientSecret).build() .setFromTokenResponse(tokenResponse); } @Override public void saveAccessTokenAttributesToUserProfile(UserProfile userProfile, OAuthCodec codec, GoogleAccessTokenContext accessToken) { GoogleTokenResponse tokenData = accessToken.getTokenData(); String encodedAccessToken = codec.encodeString(tokenData.getAccessToken()); String encodedRefreshToken = codec.encodeString(tokenData.getRefreshToken()); String encodedScope = codec.encodeString(accessToken.getScopesAsString()); OAuthPersistenceUtils.saveLongAttribute(encodedAccessToken, userProfile, OAuthConstants.PROFILE_GOOGLE_ACCESS_TOKEN, false, chunkLength); userProfile.setAttribute(OAuthConstants.PROFILE_GOOGLE_SCOPE, encodedScope); // Don't overwrite existing refresh token because it's not present in every accessToken response (only in very first one) if (encodedRefreshToken != null) { OAuthPersistenceUtils.saveLongAttribute(encodedRefreshToken, userProfile, OAuthConstants.PROFILE_GOOGLE_REFRESH_TOKEN, false, chunkLength); } } @Override public GoogleAccessTokenContext getAccessTokenFromUserProfile(UserProfile userProfile, OAuthCodec codec) { String encodedAccessToken = OAuthPersistenceUtils.getLongAttribute(userProfile, OAuthConstants.PROFILE_GOOGLE_ACCESS_TOKEN, false); String decodedAccessToken = codec.decodeString(encodedAccessToken); // We don't have token in userProfile if (decodedAccessToken == null) { return null; } String encodedRefreshToken = OAuthPersistenceUtils.getLongAttribute(userProfile, OAuthConstants.PROFILE_GOOGLE_REFRESH_TOKEN, false); String decodedRefreshToken = codec.decodeString(encodedRefreshToken); String decodedScope = codec.decodeString(userProfile.getAttribute(OAuthConstants.PROFILE_GOOGLE_SCOPE)); GoogleTokenResponse grc = new GoogleTokenResponse(); grc.setAccessToken(decodedAccessToken); grc.setRefreshToken(decodedRefreshToken); grc.setTokenType("Bearer"); grc.setExpiresInSeconds(1000L); grc.setIdToken("someTokenId"); return new GoogleAccessTokenContext(grc, decodedScope); } @Override public void removeAccessTokenFromUserProfile(UserProfile userProfile) { OAuthPersistenceUtils.removeLongAttribute(userProfile, OAuthConstants.PROFILE_GOOGLE_ACCESS_TOKEN, false); OAuthPersistenceUtils.removeLongAttribute(userProfile, OAuthConstants.PROFILE_GOOGLE_REFRESH_TOKEN, false); userProfile.setAttribute(OAuthConstants.PROFILE_GOOGLE_SCOPE, null); } @Override public void revokeToken(GoogleAccessTokenContext accessTokenContext) throws OAuthException { GoogleRequest<Void> googleRequest = new GoogleRequest<Void>() { @Override protected Void invokeRequest(GoogleAccessTokenContext accessTokenContext) throws IOException { revokeTokenImpl(accessTokenContext.getTokenData()); return null; } @Override protected OAuthException createException(IOException cause) { return new OAuthException(OAuthExceptionCode.TOKEN_REVOCATION_FAILED, "Error when revoking token", cause); } }; googleRequest.executeRequest(accessTokenContext, this); } // Send request to google without any exception handling. private void revokeTokenImpl(GoogleTokenResponse tokenData) throws IOException { TRANSPORT.createRequestFactory() .buildGetRequest(new GenericUrl("https://accounts.google.com/o/oauth2/revoke?token=" + tokenData.getAccessToken())).execute(); if (log.isTraceEnabled()) { log.trace("Revoked token " + tokenData); } } @Override public void refreshToken(GoogleAccessTokenContext accessTokenContext) { GoogleTokenResponse tokenData = accessTokenContext.getTokenData(); if (tokenData.getRefreshToken() == null) { throw new OAuthException(OAuthExceptionCode.GOOGLE_ERROR, "Given GoogleTokenResponse does not contain refreshToken"); } try { GoogleRefreshTokenRequest refreshTokenRequest = new GoogleRefreshTokenRequest(TRANSPORT, JSON_FACTORY, tokenData.getRefreshToken(), this.clientID, this.clientSecret); GoogleTokenResponse refreshed = refreshTokenRequest.execute(); // Update only 'accessToken' with new value tokenData.setAccessToken(refreshed.getAccessToken()); if (log.isTraceEnabled()) { log.trace("AccessToken refreshed successfully with value " + refreshed.getAccessToken()); } } catch (IOException ioe) { throw new OAuthException(OAuthExceptionCode.GOOGLE_ERROR, ioe); } } // Parse given String and add all scopes from it to given Set private void addScopesFromString(String scope, Set<String> scopes) { String[] scopes2 = scope.split(" "); for (String current : scopes2) { scopes.add(current); } } }