/* * JBoss, Home of Professional Open Source * * Copyright 2013 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.picketlink.social.standalone.google; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; 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 org.apache.log4j.Logger; import org.picketlink.social.standalone.oauth.OAuthConstants; import org.picketlink.social.standalone.oauth.SocialException; import org.picketlink.social.standalone.oauth.SocialExceptionCode; /** * Processor to perform Google+ interaction with usage of OAuth2 * * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> */ public class GoogleProcessor { protected static Logger log = Logger.getLogger(GoogleProcessor.class); private final String returnURL; private final String clientID; private final String clientSecret; private final Set<String> scopes = new HashSet<String>(); private final String accessType; private final String applicationName; /** 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(); /** Secure random to generate random states */ private final SecureRandom secureRandom; public GoogleProcessor(String clientID, String clientSecret, String returnURL, String accessType, String applicationName, String randomAlgorithm, String scope) { checkNotNullParam("clientID", clientID); checkNotNullParam("clientSecret", clientSecret); checkNotNullParam("returnURL", returnURL); this.clientID = clientID; this.clientSecret = clientSecret; this.returnURL = returnURL; this.accessType = accessType != null ? accessType : "offline"; this.applicationName = applicationName != null ? applicationName : "someApp"; if (randomAlgorithm == null) { randomAlgorithm = "SHA1PRNG"; } try { this.secureRandom = SecureRandom.getInstance(randomAlgorithm); } catch (NoSuchAlgorithmException nsae) { throw new IllegalArgumentException("Can't create secureRandom", nsae); } if (scope == null) { scope = "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"; } addScopesFromString(scope, this.scopes); if (log.isTraceEnabled()) { log.trace("configuration: clientId=" + clientID + ", clientSecret=" + clientSecret + ", returnURL=" + returnURL + ", scope=" + scopes + ", accessType=" + accessType + ", applicationName=" + applicationName + ", randomAlgorithm=" + randomAlgorithm); } } public InteractionState processOAuthInteraction(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, SocialException { return processOAuthInteractionImpl(httpRequest, httpResponse, this.scopes); } protected InteractionState processOAuthInteractionImpl(HttpServletRequest request, HttpServletResponse response, Set<String> scopes) throws IOException, SocialException { HttpSession session = request.getSession(); String state = (String) session.getAttribute(GoogleConstants.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(GoogleConstants.ATTRIBUTE_AUTH_STATE); session.removeAttribute(GoogleConstants.ATTRIBUTE_VERIFICATION_STATE); return new InteractionState(InteractionState.State.FINISH, accessTokenContext); } // Likely shouldn't happen... return new InteractionState(InteractionState.State.valueOf(state), null); } protected InteractionState initialInteraction(HttpServletRequest request, HttpServletResponse response, Set<String> scopes) throws IOException { String verificationState = generateSecureString(); String authorizeUrl = new GoogleAuthorizationCodeRequestUrl(clientID, returnURL, 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(GoogleConstants.ATTRIBUTE_VERIFICATION_STATE, verificationState); session.setAttribute(GoogleConstants.ATTRIBUTE_AUTH_STATE, InteractionState.State.AUTH.name()); response.sendRedirect(authorizeUrl); return new InteractionState(InteractionState.State.AUTH, null); } protected GoogleTokenResponse obtainAccessToken(HttpServletRequest request) throws SocialException { HttpSession session = request.getSession(); String stateFromSession = (String)session.getAttribute(GoogleConstants.ATTRIBUTE_VERIFICATION_STATE); String stateFromRequest = request.getParameter(OAuthConstants.STATE_PARAMETER); if (stateFromSession == null || stateFromRequest == null || !stateFromSession.equals(stateFromRequest)) { throw new SocialException(SocialExceptionCode.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 SocialException(SocialExceptionCode.USER_DENIED_SCOPE, error); } else { throw new SocialException(SocialExceptionCode.UNKNOWN_ERROR, error); } } else { String code = request.getParameter(OAuthConstants.CODE_PARAMETER); GoogleTokenResponse tokenResponse; try { tokenResponse = new GoogleAuthorizationCodeTokenRequest(TRANSPORT, JSON_FACTORY, clientID, clientSecret, code, returnURL).execute(); } catch (IOException ioe) { throw new SocialException(SocialExceptionCode.INVALID_CLIENT, "Error when obtaining access token from Google: " + ioe.getMessage(), ioe); } if (log.isTraceEnabled()) { log.trace("Successfully obtained accessToken from google: " + tokenResponse); } return tokenResponse; } } public GoogleAccessTokenContext validateTokenAndUpdateScopes(GoogleAccessTokenContext accessTokenContext) throws SocialException { GoogleRequest<Tokeninfo> googleRequest = new GoogleRequest<Tokeninfo>() { @Override protected Tokeninfo invokeRequest(GoogleAccessTokenContext accessTokenContext) throws IOException { GoogleTokenResponse tokenData = accessTokenContext.getTokenData(); Oauth2 oauth2 = getOAuth2Instance(accessTokenContext); GoogleCredential credential = getGoogleCredential(tokenData); return oauth2.tokeninfo().setAccessToken(credential.getAccessToken()).execute(); } @Override protected SocialException createException(IOException cause) { if (cause instanceof HttpResponseException) { return new SocialException(SocialExceptionCode.ACCESS_TOKEN_ERROR, "Error when obtaining tokenInfo: " + cause.getMessage(), cause); } else { return new SocialException(SocialExceptionCode.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(OAuthConstants.ERROR_PARAMETER)) { throw new SocialException(SocialExceptionCode.ACCESS_TOKEN_ERROR, "Error during token validation: " + tokenInfo.get("error").toString()); } if (!tokenInfo.getIssuedTo().equals(clientID)) { throw new SocialException(SocialExceptionCode.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); } return new GoogleAccessTokenContext(accessTokenContext.getTokenData(), tokenInfo.getScope()); } public Userinfo obtainUserInfo(GoogleAccessTokenContext accessTokenContext) throws SocialException { 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 SocialException createException(IOException cause) { if (cause instanceof HttpResponseException) { return new SocialException(SocialExceptionCode.ACCESS_TOKEN_ERROR, "Error when obtaining userInfo: " + cause.getMessage(), cause); } else { return new SocialException(SocialExceptionCode.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; } public Oauth2 getOAuth2Instance(GoogleAccessTokenContext accessTokenContext) { GoogleTokenResponse tokenData = accessTokenContext.getTokenData(); GoogleCredential credential = getGoogleCredential(tokenData); return new Oauth2.Builder(TRANSPORT, JSON_FACTORY, credential).setApplicationName(applicationName).build(); } private GoogleCredential getGoogleCredential(GoogleTokenResponse tokenResponse) { return new GoogleCredential.Builder() .setJsonFactory(JSON_FACTORY) .setTransport(TRANSPORT) .setClientSecrets(clientID, clientSecret).build() .setFromTokenResponse(tokenResponse); } /** * Revoke existing access token, so it won't be valid anymore. Application will be removed from list of existing apps of this user * on Google * * @param accessTokenContext * @throws SocialException */ public void revokeToken(GoogleAccessTokenContext accessTokenContext) throws SocialException { GoogleRequest<Void> googleRequest = new GoogleRequest<Void>() { @Override protected Void invokeRequest(GoogleAccessTokenContext accessTokenContext) throws IOException { GoogleTokenResponse tokenData = accessTokenContext.getTokenData(); TRANSPORT.createRequestFactory() .buildGetRequest(new GenericUrl("https://accounts.google.com/o/oauth2/revoke?token=" + tokenData.getAccessToken())).execute(); if (log.isTraceEnabled()) { log.trace("Revoked token " + tokenData); } return null; } @Override protected SocialException createException(IOException cause) { return new SocialException(SocialExceptionCode.TOKEN_REVOCATION_FAILED, "Error when revoking token", cause); } }; googleRequest.executeRequest(accessTokenContext, this); } /** * Refresh existing access token. Parameter must have attached refreshToken. New refreshed accessToken will be updated to this * instance of accessTokenContext * * @param accessTokenContext with refreshToken attached */ public void refreshToken(GoogleAccessTokenContext accessTokenContext) { GoogleTokenResponse tokenData = accessTokenContext.getTokenData(); if (tokenData.getRefreshToken() == null) { throw new SocialException(SocialExceptionCode.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 SocialException(SocialExceptionCode.GOOGLE_ERROR, ioe.getMessage(), ioe); } } private void addScopesFromString(String scope, Set<String> scopes) { String[] scopes2 = scope.split(" "); for (String current : scopes2) { scopes.add(current); } } private void checkNotNullParam(String paramName, String paramValue) { if (paramValue == null) { throw new IllegalArgumentException("Parameter '" + paramName + "' must be not null"); } } protected String generateSecureString() { return String.valueOf(secureRandom.nextLong()); } }