/******************************************************************************* * Copyright (c) 2012, 2013 IBM Corporation. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Eclipse Distribution License v. 1.0 which accompanies this distribution. * * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.lyo.server.oauth.webapp.services; import java.io.IOException; import java.net.InetAddress; import java.net.URISyntaxException; import java.util.List; import java.util.UUID; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriBuilder; import net.oauth.OAuth; import net.oauth.OAuth.Parameter; import net.oauth.OAuthAccessor; import net.oauth.OAuthException; import net.oauth.OAuthMessage; import net.oauth.OAuthProblemException; import net.oauth.OAuthValidator; import net.oauth.server.OAuthServlet; import org.apache.wink.json4j.JSON; import org.apache.wink.json4j.JSONException; import org.apache.wink.json4j.JSONObject; import org.eclipse.lyo.server.oauth.core.Application; import org.eclipse.lyo.server.oauth.core.AuthenticationException; import org.eclipse.lyo.server.oauth.core.OAuthConfiguration; import org.eclipse.lyo.server.oauth.core.OAuthRequest; import org.eclipse.lyo.server.oauth.core.consumer.ConsumerStoreException; import org.eclipse.lyo.server.oauth.core.consumer.LyoOAuthConsumer; import org.eclipse.lyo.server.oauth.core.token.TokenStrategy; /** * Issues OAuth request tokens, handles authentication, and then exchanges * request tokens for access tokens based on the OAuth configuration set in the * {@link OAuthConfiguration} singleton. * * @author Samuel Padgett <spadgett@us.ibm.com> * @see <a href="http://tools.ietf.org/html/rfc5849">The OAuth 1.0 Protocol</a> */ @Path("/oauth") public class OAuthService { @Context protected HttpServletRequest httpRequest; @Context protected HttpServletResponse httpResponse; @GET @Path("/requestToken") public Response doGetRequestToken() throws IOException, ServletException { return doPostRequestToken(); } /** * Responds with a request token and token secret. * * @return the response * @throws IOException * on I/O errors * @throws ServletException * on servlet errors */ @POST @Path("/requestToken") public Response doPostRequestToken() throws IOException, ServletException { try { OAuthRequest oAuthRequest = validateRequest(); // Generate the token. OAuthConfiguration.getInstance().getTokenStrategy() .generateRequestToken(oAuthRequest); // Check for OAuth 1.0a authentication. boolean callbackConfirmed = confirmCallback(oAuthRequest); // Respond to the consumer. OAuthAccessor accessor = oAuthRequest.getAccessor(); return respondWithToken(accessor.requestToken, accessor.tokenSecret, callbackConfirmed); } catch (OAuthException e) { return respondWithOAuthProblem(e); } } protected boolean confirmCallback(OAuthRequest oAuthRequest) throws OAuthException { boolean callbackConfirmed = OAuthConfiguration .getInstance() .getTokenStrategy() .getCallback(httpRequest, oAuthRequest.getAccessor().requestToken) != null; if (callbackConfirmed) { oAuthRequest.getConsumer().setOAuthVersion( LyoOAuthConsumer.OAuthVersion.OAUTH_1_0A); } else { if (!OAuthConfiguration.getInstance().isV1_0Allowed()) { throw new OAuthProblemException( OAuth.Problems.OAUTH_PARAMETERS_ABSENT); } oAuthRequest.getConsumer().setOAuthVersion( LyoOAuthConsumer.OAuthVersion.OAUTH_1_0); } return callbackConfirmed; } /* * TODO: Give providers a way to show their own branded login page. */ /** * Responds with a web page to log in. * * @return the response * @throws IOException * on I/O errors * @throws ServletException * on internal errors validating the request */ @GET @Path("/authorize") public Response authorize() throws ServletException, IOException { try { /* * Check that the request token is valid and determine what consumer * it's for. The OAuth spec does not require that consumers pass the * consumer key to the authorization page, so we must track this in * the TokenStrategy implementation. */ OAuthMessage message = OAuthServlet.getMessage(httpRequest, null); OAuthConfiguration config = OAuthConfiguration.getInstance(); String consumerKey = config.getTokenStrategy() .validateRequestToken(httpRequest, message); LyoOAuthConsumer consumer = OAuthConfiguration.getInstance() .getConsumerStore().getConsumer(consumerKey); // Pass some data to the JSP. httpRequest.setAttribute("requestToken", message.getToken()); httpRequest.setAttribute("consumerName", consumer.getName()); httpRequest.setAttribute("callback", getCallbackURL(message, consumer)); boolean callbackConfirmed = consumer.getOAuthVersion() == LyoOAuthConsumer.OAuthVersion.OAUTH_1_0A; httpRequest.setAttribute("callbackConfirmed", new Boolean( callbackConfirmed)); // The application name is displayed on the OAuth login page. httpRequest.setAttribute("applicationName", config.getApplication().getName()); httpResponse.setHeader(HTTPConstants.HDR_CACHE_CONTROL, HTTPConstants.NO_CACHE); if (config.getApplication().isAuthenticated(httpRequest)) { // Show the grant access page. httpRequest.getRequestDispatcher("/oauth/authorize.jsp").forward( httpRequest, httpResponse); } else { // Show the login page. httpRequest.getRequestDispatcher("/oauth/login.jsp").forward( httpRequest, httpResponse); } return null; } catch (OAuthException e) { return respondWithOAuthProblem(e); } } private String getCallbackURL(OAuthMessage message, LyoOAuthConsumer consumer) throws IOException, OAuthException { String callback = null; switch (consumer.getOAuthVersion()) { case OAUTH_1_0: if (!OAuthConfiguration.getInstance().isV1_0Allowed()) { throw new OAuthProblemException(OAuth.Problems.VERSION_REJECTED); } // If this is OAuth 1.0, the callback should be a request parameter. callback = message.getParameter(OAuth.OAUTH_CALLBACK); break; case OAUTH_1_0A: // If this is OAuth 1.0a, the callback was passed when the consumer // asked for a request token. String requestToken = message.getToken(); callback = OAuthConfiguration.getInstance().getTokenStrategy() .getCallback(httpRequest, requestToken); } if (callback == null) { return null; } UriBuilder uriBuilder = UriBuilder.fromUri(callback) .queryParam(OAuth.OAUTH_TOKEN, message.getToken()); if (consumer.getOAuthVersion() == LyoOAuthConsumer.OAuthVersion.OAUTH_1_0A) { String verificationCode = OAuthConfiguration.getInstance() .getTokenStrategy() .generateVerificationCode(httpRequest, message.getToken()); uriBuilder.queryParam(OAuth.OAUTH_VERIFIER, verificationCode); } return uriBuilder.build().toString(); } /** * Validates the ID and password on the authorization form. This is intended * to be invoked by an XHR on the login page. * * @return the response, 409 if login failed or 204 if successful */ @POST @Path("/login") public Response login(@FormParam("id") String id, @FormParam("password") String password, @FormParam("requestToken") String requestToken) { CSRFPrevent.check(httpRequest); try { OAuthConfiguration.getInstance().getApplication() .login(httpRequest, id, password); } catch (OAuthException e) { return Response.status(Status.SERVICE_UNAVAILABLE).build(); } catch (AuthenticationException e) { String message = e.getMessage(); if (message == null || "".equals(message)) { message = "Incorrect username or password."; } return Response.status(Status.CONFLICT).entity(message) .type(MediaType.TEXT_PLAIN).build(); } try { OAuthConfiguration.getInstance().getTokenStrategy() .markRequestTokenAuthorized(httpRequest, requestToken); } catch (OAuthException e) { return Response.status(Status.CONFLICT) .entity("Request token invalid.") .type(MediaType.TEXT_PLAIN).build(); } return Response.noContent().build(); } @POST @Path("/internal/approveToken") public Response authorize(@FormParam("requestToken") String requestToken) { CSRFPrevent.check(httpRequest); try { if (!OAuthConfiguration.getInstance().getApplication().isAuthenticated(httpRequest)) { return Response.status(Status.FORBIDDEN).build(); } } catch (OAuthProblemException e) { return Response.status(Status.SERVICE_UNAVAILABLE).build(); } return authorizeToken(requestToken); } private Response authorizeToken(String requestToken) { try { OAuthConfiguration.getInstance().getTokenStrategy() .markRequestTokenAuthorized(httpRequest, requestToken); } catch (OAuthException e) { return Response.status(Status.CONFLICT) .entity("Request token invalid.") .type(MediaType.TEXT_PLAIN).build(); } return Response.noContent().build(); } @GET @Path("/accessToken") public Response doGetAccessToken() throws IOException, ServletException { return doPostAccessToken(); } /** * Responds with an access token and token secret for valid OAuth requests. * The request must be signed and the request token valid. * * @return the response * @throws IOException * on I/O errors * @throws ServletException * on servlet errors */ @POST @Path("/accessToken") public Response doPostAccessToken() throws IOException, ServletException { try { // Validate the request is signed and check that the request token // is valid. OAuthRequest oAuthRequest = validateRequest(); OAuthConfiguration config = OAuthConfiguration.getInstance(); TokenStrategy strategy = config.getTokenStrategy(); strategy.validateRequestToken(httpRequest, oAuthRequest.getMessage()); // The verification code MUST be passed in the request if this is // OAuth 1.0a. if (!config.isV1_0Allowed() || oAuthRequest.getConsumer().getOAuthVersion() == LyoOAuthConsumer.OAuthVersion.OAUTH_1_0A) { strategy.validateVerificationCode(oAuthRequest); } // Generate a new access token for this accessor. strategy.generateAccessToken(oAuthRequest); // Send the new token and secret back to the consumer. OAuthAccessor accessor = oAuthRequest.getAccessor(); return respondWithToken(accessor.accessToken, accessor.tokenSecret); } catch (OAuthException e) { return respondWithOAuthProblem(e); } } /** * Generates a provisional consumer key. This request must be later approved * by an administrator. * * @return a JSON response with the provisional key * @throws IOException * @throws NullPointerException * @see <a href="https://jazz.net/wiki/bin/view/Main/RootServicesSpecAddendum2">Jazz Root Services Spec Addendum2</a> */ @POST @Path("/requestKey") // Some consumers do not set an appropriate Content-Type header. //@Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) public Response provisionalKey() throws NullPointerException, IOException { try { // Create the consumer from the request. JSONObject request = (JSONObject) JSON.parse(httpRequest .getInputStream()); String name = null; if (request.has("name") && request.get("name") != null) { name = request.getString("name"); } if (name == null || name.trim().equals("")) { name = getRemoteHost(); } String secret = request.getString("secret"); boolean trusted = false; if (request.has("trusted")) { trusted = "true".equals(request.getString("trusted")); } String key = UUID.randomUUID().toString(); LyoOAuthConsumer consumer = new LyoOAuthConsumer(key, secret); consumer.setName(name); consumer.setProvisional(true); consumer.setTrusted(trusted); // Add the consumer to the store. OAuthConfiguration.getInstance().getConsumerStore().addConsumer(consumer); // Respond with the consumer key. JSONObject response = new JSONObject(); response.put("key", key); return Response.ok(response.write()) .header(HTTPConstants.HDR_CACHE_CONTROL, HTTPConstants.NO_CACHE).build(); } catch (JSONException e) { e.printStackTrace(); return Response.status(Status.BAD_REQUEST).build(); } catch (ConsumerStoreException e) { e.printStackTrace(); return Response.status(Status.SERVICE_UNAVAILABLE) .type(MediaType.TEXT_PLAIN).entity(e.getMessage()).build(); } } /** * Shows the approval page for a single provisional consumer. Shows the * consumer management page instead if no key is passed in. * * @param key * the consumer * @return the approve consumer page * @throws ServletException * on errors showing the JSP * @throws IOException * on errors showing the JSP * @see #showConsumerKeyManagementPage() */ @GET @Path("/approveKey") @Produces({ MediaType.TEXT_HTML }) public Response showApproveKeyPage(@QueryParam("key") String key) throws ServletException, IOException { if (key == null || "".equals(key)) { return showConsumerKeyManagementPage(); } try { Application app = OAuthConfiguration.getInstance().getApplication(); // The application name is displayed on approval page. httpRequest.setAttribute("applicationName", app.getName()); if (!app.isAdminSession(httpRequest)) { return showAdminLogin(); } LyoOAuthConsumer provisionalConsumer = OAuthConfiguration .getInstance().getConsumerStore().getConsumer(key); if (provisionalConsumer == null) { return Response.status(Status.BAD_REQUEST).build(); } httpResponse.setHeader(HTTPConstants.HDR_CACHE_CONTROL, HTTPConstants.NO_CACHE); httpRequest.setAttribute("consumerName", provisionalConsumer.getName()); httpRequest.setAttribute("consumerKey", provisionalConsumer.consumerKey); httpRequest .setAttribute("trusted", provisionalConsumer.isTrusted()); final String dispatchTo = (provisionalConsumer.isProvisional()) ? "/oauth/approveKey.jsp" : "/oauth/keyAlreadyApproved.jsp"; httpRequest.getRequestDispatcher(dispatchTo).forward(httpRequest, httpResponse); return null; } catch (ConsumerStoreException e) { e.printStackTrace(); return Response.status(Status.CONFLICT).type(MediaType.TEXT_PLAIN) .entity(e.getMessage()).build(); } catch (OAuthProblemException e) { return respondWithOAuthProblem(e); } } /** * Shows the consumer management page, which allows administrator to approve * or remove OAuth consumers. * * @return the consumer management page * @throws ServletException * on JSP errors * @throws IOException * on JSP errors */ @GET @Path("/admin") public Response showConsumerKeyManagementPage() throws ServletException, IOException { try { Application app = OAuthConfiguration.getInstance().getApplication(); httpRequest.setAttribute("applicationName", app.getName()); if (!app.isAdminSession(httpRequest)) { return showAdminLogin(); } } catch (OAuthException e) { return Response.status(Status.SERVICE_UNAVAILABLE).build(); } httpResponse.setHeader(HTTPConstants.HDR_CACHE_CONTROL, HTTPConstants.NO_CACHE); httpRequest.getRequestDispatcher("/oauth/manage.jsp").forward( httpRequest, httpResponse); return null; } /** * Validates that the ID and password are for an administrator. This is used * by the admin login page to protect the OAuth administration pages. * * @return the response, 409 if login failed or 204 if successful */ @POST @Path("/adminLogin") public Response login(@FormParam("id") String id, @FormParam("password") String password) { CSRFPrevent.check(httpRequest); try { Application app = OAuthConfiguration.getInstance().getApplication(); app.login(httpRequest, id, password); if (app.isAdminSession(httpRequest)) { return Response.noContent().build(); } return Response.status(Status.CONFLICT) .entity("The user '" + id + "' is not an administrator.") .type(MediaType.TEXT_PLAIN).build(); } catch (OAuthException e) { return Response.status(Status.SERVICE_UNAVAILABLE).build(); } catch (AuthenticationException e) { String message = e.getMessage(); if (message == null || "".equals(message)) { message = "Incorrect username or password."; } return Response.status(Status.CONFLICT).entity(message) .type(MediaType.TEXT_PLAIN).build(); } } /** * Validates this is a known consumer and the request is valid using * {@link OAuthValidator#validateMessage(net.oauth.OAuthMessage, OAuthAccessor)}. * Does <b>not</b> check for any tokens. * * @return an OAuthRequest * @throws OAuthException * if the request fails validation * @throws IOException * on I/O errors */ protected OAuthRequest validateRequest() throws OAuthException, IOException { OAuthRequest oAuthRequest = new OAuthRequest(httpRequest); try { OAuthValidator validator = OAuthConfiguration.getInstance() .getValidator(); validator.validateMessage(oAuthRequest.getMessage(), oAuthRequest.getAccessor()); } catch (URISyntaxException e) { throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR); } return oAuthRequest; } protected Response respondWithToken(String token, String tokenSecret) throws IOException { return respondWithToken(token, tokenSecret, false); } protected Response respondWithToken(String token, String tokenSecret, boolean callbackConfirmed) throws IOException { List<Parameter> oAuthParameters = OAuth.newList(OAuth.OAUTH_TOKEN, token, OAuth.OAUTH_TOKEN_SECRET, tokenSecret); if (callbackConfirmed) { oAuthParameters.add(new Parameter(OAuth.OAUTH_CALLBACK_CONFIRMED, "true")); } String responseBody = OAuth.formEncode(oAuthParameters); return Response.ok(responseBody) .type(MediaType.APPLICATION_FORM_URLENCODED) .header(HTTPConstants.HDR_CACHE_CONTROL, HTTPConstants.NO_CACHE).build(); } protected Response respondWithOAuthProblem(OAuthException e) throws IOException, ServletException { try { OAuthServlet.handleException(httpResponse, e, OAuthConfiguration .getInstance().getApplication().getRealm(httpRequest)); } catch (OAuthProblemException serviceUnavailableException) { return Response.status(Status.SERVICE_UNAVAILABLE).build(); } return Response.status(Status.UNAUTHORIZED).build(); } private String getRemoteHost() { try { // Try to get the hostname of the consumer. return InetAddress.getByName(httpRequest.getRemoteHost()) .getCanonicalHostName(); } catch (Exception e) { /* * Not fatal, and we shouldn't fail here. Fall back to returning * ServletRequest.getRemoveHost(). It might be the IP address, but * that's better than nothing. */ return httpRequest.getRemoteHost(); } } private Response showAdminLogin() throws ServletException, IOException { httpResponse.setHeader(HTTPConstants.HDR_CACHE_CONTROL, HTTPConstants.NO_CACHE); StringBuffer callback = httpRequest.getRequestURL(); String query = httpRequest.getQueryString(); if (query != null) { callback.append('?'); callback.append(query); } httpRequest.setAttribute("callback", callback.toString()); httpRequest.getRequestDispatcher("/oauth/adminLogin.jsp").forward( httpRequest, httpResponse); return null; } }