package org.vaadin.oauth; import java.io.IOException; import java.lang.reflect.Field; import org.scribe.builder.ServiceBuilder; import org.scribe.builder.api.Api; import org.scribe.model.OAuthRequest; import org.scribe.model.Response; import org.scribe.model.Token; import org.scribe.model.Verb; import org.scribe.model.Verifier; import org.scribe.oauth.OAuthService; import com.google.gson.Gson; import com.vaadin.server.Page; import com.vaadin.server.RequestHandler; import com.vaadin.server.VaadinRequest; import com.vaadin.server.VaadinResponse; import com.vaadin.server.VaadinSession; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.ui.BorderStyle; import com.vaadin.ui.Button; /** * Starting point to create a {@link Button} that allows the user to log in * using OAuth; e.g log in with Facebook or Twitter. * <p> * Uses the Scribe oauth library, and it should be fairly straightforward to * implement a button for all supported services. * </p> * <p> * Generally, you just give the buttons the API keys that can be obtained from * the service in question, and a callback that will receive some user data once * the user has been authenticated. Some buttons implementations might provide * additional options (e.g get user email address from Facebook). * </p> * <p> * This approach is intentionally simplistic for this specific use-case: log in * with X. For more flexible OAuth interactions, the Scribe library can be used * directly. * </p> * */ public abstract class OAuthButton extends Button { protected OAuthService service = null; protected Token requestToken = null; protected Token accessToken = null; protected String apiKey; protected String apiSecret; protected RequestHandler handler; protected OAuthListener authListener; private Page callbackPage; /** * @param caption * button caption * @param apiKey * API key from the service providing OAuth * @param apiSecret * API secret from the service providing OAuth * @param authListener * called once the user has been authenticated */ public OAuthButton(String caption, String apiKey, String apiSecret, OAuthListener authListener) { super(caption); this.apiKey = apiKey; this.apiSecret = apiSecret; this.authListener = authListener; } @Override protected void fireClick() { super.fireClick(); authenticate(); } protected void fireClick(MouseEventDetails details) { fireEvent(new Button.ClickEvent(this, details)); authenticate(); } /** * Gets the URL that the user will be sent to in order to authenticate. Most * implementations will also create the requestToken at this point. * * @return authentication url for the OAuth service */ protected abstract String getAuthUrl(); /** * Gets the {@link Api} implementation class that this service uses. * * @return {@link Api} implementation class */ protected abstract Class<? extends Api> getApi(); /** * Gets the name of the parameter that will contain the verifier when the * user returns from the OAuth service. * * @return verifier parameter name */ protected abstract String getVerifierName(); private static final String[] oauthFails = new String[] { "oauth_problem" }; /** * Gets the names of parameters that the OAuth service uses to indicate a * problem during authentication - e.g if the user presses 'Cancel' at the * authentication page. * * @return */ protected String[] getFailureParameters() { return oauthFails; } /** * Gets the URL from which JSON formatted user data can be fetched. * * @return JSON user data url */ protected abstract String getJsonDataUrl(); /** * Gets the {@link User} implementation class for the user data that this * service provides. * * @return {@link User} implementation class */ protected abstract Class<? extends User> getUserClass(); /** * Gets the OAuth service singleton. * * @return OAuth service singleton */ protected OAuthService getService() { if (service == null) { callbackPage = Page.getCurrent(); String location = callbackPage.getLocation().toString(); Class<? extends Api> api = getApi(); ServiceBuilder builder = new ServiceBuilder(); builder.provider(api); builder.apiKey(apiKey); builder.apiSecret(apiSecret); builder.callback(location); service=builder.build(); } return service; } /** * Connects the parameter handler that will be invoked when the user comes * back, and sends the user to the authentication url for the OAuth service. */ protected void authenticate() { if (handler == null) { handler = createRequestHandler(); VaadinSession.getCurrent().addRequestHandler(handler); } //opens authentificationpage String url = getAuthUrl(); this.callbackPage.open(url, "Authentificate", 400, 300, BorderStyle.DEFAULT); } /** * Creates the parameter handler that will be invoked when the user returns * from the OAuth service. * * @return the parameter handler */ protected RequestHandler createRequestHandler() { return new RequestHandler() { public boolean handleRequest(VaadinSession session, VaadinRequest request, VaadinResponse response) throws IOException { if (request.getParameterMap().containsKey(getVerifierName())) { String v = request.getParameter(getVerifierName()); Verifier verifier = new Verifier(v); accessToken = service .getAccessToken(requestToken, verifier); User user = getUser(); VaadinSession.getCurrent().removeRequestHandler(handler); handler = null; //String url = getAuthUrl(); //callbackPage.open(url, "Authentificate", 400, 300, BorderStyle.DEFAULT); authListener.userAuthenticated(user); } else if (getFailureParameters() != null) { for (String key : getFailureParameters()) { if (request.getParameterMap().containsKey(key)) { authListener.failed(request.getParameter(key)); break; } } } return true; } }; } /** * Creates and returns the {@link User} instance, usually by retreiving JSON * data from the url provided by {@link #getJsonDataUrl()}. * * @return the {@link User} instance containing user data from the service */ protected User getUser() { OAuthRequest request = new OAuthRequest(Verb.GET, getJsonDataUrl()); service.signRequest(accessToken, request); Response response = request.send(); Gson gson = new Gson(); User user = gson.fromJson(response.getBody(), getUserClass()); // TODO set the token/secret here? try { Field tokenField = user.getClass().getDeclaredField("token"); if (tokenField != null) { tokenField.setAccessible(true); tokenField.set(user, accessToken.getToken()); } Field tokenSecretField = user.getClass().getDeclaredField( "tokenSecret"); if (tokenSecretField != null) { tokenSecretField.setAccessible(true); tokenSecretField.set(user, accessToken.getSecret()); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } return user; } /** * Called when the {@link User} instance has been successfully created, or * the OAuth service returned a problem code. */ public interface OAuthListener { public void userAuthenticated(User user); public void failed(String reason); } /** * Contains user data common for most services. Some services might add own * data, or leave some data as null - for instance 'email' is quite seldom * available trough the APIs. * <p> * The default {@link OAuthButton#getUser()} implementation sets the 'token' * and 'tokenSecret' member fields if such exist, so that the {@link User} * implementation can just return these in {@link #getToken()} and * {@link #getTokenSecret()}. * </p> */ public static interface User { /** * Name of the OAuth service, e.g "facebook". * * @return */ public String getService(); /** * Often "Firstname Lastname", but not always - e.g Twitter users have a * single 'name' that can be changed to pretty much anything. * * @return user name */ public String getName(); /** * The screen name is usually a short username used no the service, most * often unique, and quite often used to identify the user profile (e.g * http://twitter.com/screenname). * * @return */ public String getScreenName(); /** * Url to the avatar picture for the user. * * @return */ public String getPictureUrl(); /** * Id form the OAuth service; this is unique within the service. A * "globaly unique" id can be created for instance by combining this id * with the service name (e.g "facebook:12345"). * * @return */ public String getId(); /** * Url to the users public profile on the service (e.g * http://twitter.com/screenname). * * @return */ public String getPublicProfileUrl(); /** * Email address - NOTE that this is quite seldom provided. Also, it * might be better to allow the user to provide an email address of * choice while registering for your service. * * @return email address or (quite often) null */ public String getEmail(); /** * Gets the OAuth access token that can be used together with the token * secret ({@link #getTokenSecret()}) in order to access the OAuth * service API. * * @return OAuth access token */ public String getToken(); /** * Gets the OAuth access token secret that can be used together with the * token ({@link #getToken()}) in order to access the OAuth service API. * * @return OAuth access token secret */ public String getTokenSecret(); } }