Java tutorial
/* * Copyright 2013 Google Inc. All Rights Reserved. * * 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 com.google.plus.samples.photohunt; import com.google.api.client.auth.oauth2.TokenResponseException; 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.GoogleTokenResponse; import com.google.api.services.oauth2.Oauth2; import com.google.api.services.oauth2.model.Tokeninfo; import com.google.api.services.plus.Plus; import com.google.api.services.plus.model.PeopleFeed; import com.google.api.services.plus.model.Person; import com.google.gson.annotations.Expose; import com.google.plus.samples.photohunt.model.DirectedUserToUserEdge; import com.google.plus.samples.photohunt.model.Jsonifiable; import com.google.plus.samples.photohunt.model.User; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.List; import static com.google.plus.samples.photohunt.model.OfyService.ofy; /** * Provides an API to connect users to Photohunt. This servlet provides the * /api/connect end-point, and exposes the following operations: * <p> * POST /api/connect * * @author silvano@google.com (Silvano Luciani) */ public class ConnectServlet extends JsonRestServlet { /** * Exposed as `POST /api/connect`. * <p> * Takes the following payload in the request body. Payload represents all * parameters required to authorize and/or connect. * { * "state":"", * "access_token":"", * "token_type":"", * "expires_in":"", * "code":"", * "id_token":"", * "authuser":"", * "session_state":"", * "prompt":"", * "client_id":"", * "scope":"", * "g_user_cookie_policy":"", * "cookie_policy":"", * "issued_at":"", * "expires_at":"", * "g-oauth-window":"" * } * <p> * Returns the following JSON response representing the User that was * connected: * { * "id":0, * "googleUserId":"", * "googleDisplayName":"", * "googlePublicProfileUrl":"", * "googlePublicProfilePhotoUrl":"", * "googleExpiresAt":0 * } * <p> * Issues the following errors along with corresponding HTTP response codes: * 401: error from token verification end-point. * 500: "Failed to upgrade the authorization code." (for code exchange flows) * 500: "Failed to read token data from Google." * + error from reading token verification response. * 500: "Failed to query the Google+ API: " + error from client library. * 500: IOException occurred (several ways this could happen). * * @see javax.servlet.http.HttpServlet#doPost( *javax.servlet.http.HttpServletRequest, * javax.servlet.http.HttpServletResponse) */ @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { TokenData accessToken = null; try { // read the token accessToken = Jsonifiable.fromJson(req.getReader(), TokenData.class); } catch (IOException e) { sendError(resp, 400, "Unable to read auth result from request body"); } // Create a credential object. GoogleCredential credential = new GoogleCredential.Builder().setJsonFactory(JSON_FACTORY) .setTransport(TRANSPORT).setClientSecrets(CLIENT_ID, CLIENT_SECRET).build(); try { if (accessToken.code != null) { // exchange the code for a token (Web Frontend) GoogleTokenResponse tokenFromExchange = exchangeCode(accessToken); credential.setFromTokenResponse(tokenFromExchange); } else { // use the token received from the client credential.setAccessToken(accessToken.access_token).setRefreshToken(accessToken.refresh_token) .setExpiresInSeconds(accessToken.expires_in) .setExpirationTimeMilliseconds(accessToken.expires_at); } // ensure that we consider logged in the user that owns the access token String tokenGoogleUserId = verifyToken(credential); User user = saveTokenForUser(tokenGoogleUserId, credential); // save the user in the session HttpSession session = req.getSession(); session.setAttribute(CURRENT_USER_SESSION_KEY, user.id); generateFriends(user, credential); sendResponse(req, resp, user); } catch (TokenVerificationException e) { sendError(resp, 401, e.getMessage()); } catch (TokenResponseException e) { sendError(resp, 500, "Failed to upgrade the authorization code."); } catch (TokenDataException e) { sendError(resp, 500, "Failed to read token data from Google. " + e.getMessage()); } catch (IOException e) { sendError(resp, 500, e.getMessage()); } catch (GoogleApiException e) { sendError(resp, 500, "Failed to query the Google+ API: " + e.getMessage()); } } /** * Exchanges the `code` member of the given AccessToken object, and returns * the relevant GoogleTokenResponse. * * @param accessToken Container of authorization code to exchange. * @return Token response from Google indicating token information. * @throws TokenDataException Failed to exchange code (code invalid). */ private GoogleTokenResponse exchangeCode(TokenData accessToken) throws TokenDataException { try { // Upgrade the authorization code into an access and refresh token. return new GoogleAuthorizationCodeTokenRequest(TRANSPORT, JSON_FACTORY, CLIENT_ID, CLIENT_SECRET, accessToken.code, "postmessage").execute(); } catch (IOException e) { throw new TokenDataException(e.getMessage()); } } /** * Verify that the token in the given credential is valid. * * @param credential Credential to verify. * @return Google user ID for which token was issued. * @throws TokenVerificationException Credential is not valid. * @throws IOException Could not verify Credential because of a network * failure. */ private String verifyToken(GoogleCredential credential) throws TokenVerificationException, IOException { // Check that the token is valid. Oauth2 oauth2 = new Oauth2.Builder(TRANSPORT, JSON_FACTORY, credential).build(); Tokeninfo tokenInfo = oauth2.tokeninfo().setAccessToken(credential.getAccessToken()).execute(); // If there was an error in the token info, abort. if (tokenInfo.containsKey("error")) { throw new TokenVerificationException(tokenInfo.get("error").toString()); } // Make sure the token we got is for our app. if (!tokenInfo.getIssuedTo().equals(CLIENT_ID)) { throw new TokenVerificationException("Token's client ID does not match app's."); } return tokenInfo.getUserId(); } /** * Either: * 1. Create a user for the given ID and credential * 2. or, update the existing user with the existing credential * <p> * If 2, then ask Google for the user's public profile information to store. * * @param tokenGoogleUserId Google user ID to update. * @param credential Credential to set for the user. * @return Updated User. * @throws GoogleApiException Could not fetch profile info for user. */ private User saveTokenForUser(String tokenGoogleUserId, GoogleCredential credential) throws GoogleApiException { User user = ofy().load().type(User.class).filter("googleUserId", tokenGoogleUserId).first().get(); if (user == null) { // Register a new user. Collect their Google profile info first. Plus plus = new Plus.Builder(TRANSPORT, JSON_FACTORY, credential).build(); Person profile; Plus.People.Get get; try { get = plus.people().get("me"); profile = get.execute(); } catch (IOException e) { throw new GoogleApiException(e.getMessage()); } user = new User(); user.setGoogleUserId(profile.getId()); user.setGoogleDisplayName(profile.getDisplayName()); user.setGooglePublicProfileUrl(profile.getUrl()); user.setGooglePublicProfilePhotoUrl(profile.getImage().getUrl()); } // TODO(silvano): Also fetch and set the email address for the user. user.setGoogleAccessToken(credential.getAccessToken()); if (credential.getRefreshToken() != null) { user.setGoogleRefreshToken(credential.getRefreshToken()); } user.setGoogleExpiresAt(credential.getExpirationTimeMilliseconds()); user.setGoogleExpiresIn(credential.getExpiresInSeconds()); ofy().save().entity(user).now(); return user; } /** * Query Google for the list of the user's friends that they've shared with * our app, and then store those friends for later use. * * @param user User for which to get friends. * @param credential Credential to use to authorize people.list request. * @throws IOException Unable to fetch friends because of network error. */ private void generateFriends(User user, GoogleCredential credential) throws IOException { // TODO(silvano): Refactor this method to not have race condition. // TODO(silvano): Refactor this method to use task queue. // TODO(silvano): Refactor this method to fetch more than first page. // Simple but inefficient way of building the friends list Plus plus = new Plus.Builder(TRANSPORT, JSON_FACTORY, credential).build(); Plus.People.List get; List<DirectedUserToUserEdge> friends = ofy().load().type(DirectedUserToUserEdge.class) .filter("ownerUserId", user.getId()).list(); ofy().delete().entities(friends); get = plus.people().list(user.getGoogleUserId(), "visible"); PeopleFeed feed = get.execute(); for (Person googlePlusPerson : feed.getItems()) { User friend = ofy().load().type(User.class).filter("googleUserId", googlePlusPerson.getId()).first() .get(); if (friend != null) { DirectedUserToUserEdge friendEdge = new DirectedUserToUserEdge(); friendEdge.setOwnerUserId(user.getId()); friendEdge.setFriendUserId(friend.getId()); ofy().save().entity(friendEdge).now(); } } } /** * Simple Jsonifiable to represent token information sent/retrieved from our * app and its clients (web, Android, iOS). */ public static class TokenData extends Jsonifiable { public static final String kind = "photohunt#tokendata"; /** * Google access token used to authorize requests to Google. */ @Expose public String access_token; /** * Google refresh token used to get new access tokens when needed. */ @Expose public String refresh_token; /** * Authorization code used to exchange for an access/refresh token pair. */ @Expose public String code; /** * Identity token for this user. */ @Expose public String id_token; /** * When the access token expires. */ @Expose public Long expires_at; /** * How long until the access token expires. */ @Expose public Long expires_in; } /** * Thrown when token data can't be read from verification end-point. */ class TokenDataException extends Exception { public TokenDataException(String message) { super(message); } } /** * Exception thrown when errors occurs during token verification. */ class TokenVerificationException extends Exception { public TokenVerificationException(String message) { super(message); } } /** * Exception thrown when errors occurs querying the Google + API. */ class GoogleApiException extends Exception { public GoogleApiException(String message) { super(message); } } }