/******************************************************************************* * Copyright 2013 Open mHealth * * 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.openmhealth.reference.servlet; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.oltu.oauth2.as.request.OAuthAuthzRequest; import org.apache.oltu.oauth2.as.request.OAuthTokenRequest; import org.apache.oltu.oauth2.as.response.OAuthASResponse; import org.apache.oltu.oauth2.common.error.OAuthError.CodeResponse; import org.apache.oltu.oauth2.common.error.OAuthError.TokenResponse; import org.apache.oltu.oauth2.common.exception.OAuthProblemException; import org.apache.oltu.oauth2.common.exception.OAuthSystemException; import org.apache.oltu.oauth2.common.message.OAuthResponse; import org.apache.oltu.oauth2.common.message.types.GrantType; import org.apache.oltu.oauth2.common.message.types.ResponseType; import org.apache.oltu.oauth2.common.message.types.TokenType; import org.openmhealth.reference.data.AuthorizationCodeBin; import org.openmhealth.reference.data.AuthorizationCodeResponseBin; import org.openmhealth.reference.data.AuthorizationTokenBin; import org.openmhealth.reference.data.Registry; import org.openmhealth.reference.data.ThirdPartyBin; import org.openmhealth.reference.data.UserBin; import org.openmhealth.reference.domain.AuthenticationToken; import org.openmhealth.reference.domain.AuthorizationCode; import org.openmhealth.reference.domain.AuthorizationCodeResponse; import org.openmhealth.reference.domain.AuthorizationToken; import org.openmhealth.reference.domain.Data; import org.openmhealth.reference.domain.MultiValueResult; import org.openmhealth.reference.domain.ThirdParty; import org.openmhealth.reference.domain.User; import org.openmhealth.reference.exception.OmhException; import org.openmhealth.reference.filter.AuthFilter; import org.openmhealth.reference.request.AuthenticationRequest; import org.openmhealth.reference.request.DataReadRequest; import org.openmhealth.reference.request.DataWriteRequest; import org.openmhealth.reference.request.ListRequest; import org.openmhealth.reference.request.OauthRegistrationRequest; import org.openmhealth.reference.request.Request; import org.openmhealth.reference.request.SchemaIdsRequest; import org.openmhealth.reference.request.SchemaRequest; import org.openmhealth.reference.request.SchemaVersionsRequest; import org.openmhealth.reference.request.UserActivationRequest; import org.openmhealth.reference.request.UserRegistrationRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; /** * <p> * The controller for the version 1 of the Open mHealth API. * </p> * * <p> * This class has no state and, therefore, is immutable. * </p> * * @author John Jenkins */ @Controller @RequestMapping(Version1.PATH) public class Version1 { /** * The root path for queries to this version of the API. */ public static final String PATH = "/v1"; /** * The path to the user registration end-point. */ public static final String PATH_REGISTRATION = "/users/registration"; /** * The path to the user activation end-point. */ public static final String PATH_ACTIVATION = "/users/activation"; /** * The username parameter for the authenticate requests. */ public static final String PARAM_AUTHENTICATION_USERNAME = "username"; /** * The password parameter for the authenticate requests. */ public static final String PARAM_AUTHENTICATION_PASSWORD = "password"; /** * The authentication token parameter for requests that require * authentication. */ public static final String PARAM_AUTHENTICATION_AUTH_TOKEN = "omh_auth_token"; /** * The authorization flag that indicates if the user granted the * third-party access. */ public static final String PARAM_AUTHORIZATION_GRANTED = "granted"; /** * The key for the code parameter when the user is responding to a request * to grant access to a third-party. */ public static final String PARAM_AUTHORIZATION_CODE = "code"; /** * The parameter for the number of records to skip in requests that use * paging. */ public static final String PARAM_PAGING_NUM_TO_SKIP = "num_to_skip"; /** * The parameter for the number of records to return in requests that use * paging. */ public static final String PARAM_PAGING_NUM_TO_RETURN = "num_to_return"; /** * The parameter for the unique identifier for a schema. This is sometimes * used as part of the URI for the RESTful implementation. */ public static final String PARAM_SCHEMA_ID = "schema_id"; /** * The parameter for the version of a schema. This is sometimes used as * part of the URI for the RESTful implementation. */ public static final String PARAM_SCHEMA_VERSION = "schema_version"; /** * A parameter that limits the results to only those that were created on * or after the given date. */ public static final String PARAM_DATE_START = "t_start"; /** * A parameter that limits the results to only those that were created on * or before the given date. */ public static final String PARAM_DATE_END = "t_end"; /** * The parameter that indicates to which user the data should pertain. */ public static final String PARAM_OWNER = "owner"; /** * The parameter that indicates that the data should be summarized, if * possible. */ public static final String PARAM_SUMMARIZE = "summarize"; /** * The parameter that indicates which columns of the data should be * returned. */ public static final String PARAM_COLUMN_LIST = "column_list"; /** * The parameter for the data when it is being uploaded. */ public static final String PARAM_DATA = "data"; /** * The header for the URL to the previous set of data for list requests. */ public static final String HEADER_PREVIOUS = "Previous"; /** * The header for the URL to the next set of data for list requests. */ public static final String HEADER_NEXT = "Next"; /** * The encoding for the previous and next URLs. */ private static final String URL_ENCODING_UTF_8 = "UTF-8"; /** * The logger for this class. */ private static final Logger LOGGER = Logger.getLogger(Version1.class.getCanonicalName()); /** * Creates an authentication request, authenticates the user and, if * successful, returns the user's credentials. * * @param username * The username of the user attempting to authenticate. * * @param password * The password of the user attempting to authenticate. * * @param request * The HTTP request object. * * @param response * The HTTP response object. * * @return The authorization token. * * @throws OmhException * There was a problem with the request. This could be any of the * sub-classes of {@link OmhException}. */ @RequestMapping(value = "auth", method = RequestMethod.POST) public @ResponseBody String getAuthentication( @RequestParam( value = PARAM_AUTHENTICATION_USERNAME, required = true) final String username, @RequestParam( value = PARAM_AUTHENTICATION_PASSWORD, required = true) final String password, final HttpServletRequest request, final HttpServletResponse response) throws OmhException { // Create the authentication request from parameters. AuthenticationToken token = handleRequest( request, response, new AuthenticationRequest(username, password)); // Add a cookie for the authentication token. Cookie cookie = new Cookie(PARAM_AUTHENTICATION_AUTH_TOKEN, token.getToken()); // Set the expiration on the cookie. cookie .setMaxAge( new Long( (token.getExpires() - System.currentTimeMillis()) / 1000) .intValue()); // Build the path without the "auth" part. String requestUri = request.getRequestURI(); cookie.setPath(requestUri.substring(0, requestUri.length() - 5)); // Make sure the cookie is only used with HTTPS. cookie.setSecure(true); // Add the cookie to the response. response.addCookie(cookie); // Return the token. return token.getToken(); } /** * Creates a registration request for a third-party ("client" in OAuth * parlance). * * @param name * The third-party's name. * * @param description * The third-party's description. * * @param redirectUri * The location to redirect the user to after they have responded to * an authorization request from this third-party. * * @param request * The HTTP request. * * @param response * The HTTP response. */ @RequestMapping( value = "auth/oauth/registration", method = RequestMethod.POST) public @ResponseBody ThirdParty thirdPartyRegistration( @RequestParam( value = ThirdParty.JSON_KEY_NAME, required = true) final String name, @RequestParam( value = ThirdParty.JSON_KEY_DESCRIPTION, required = true) final String description, @RequestParam( value = ThirdParty.JSON_KEY_REDIRECT_URI, required = true) final String redirectUri, final HttpServletRequest request, final HttpServletResponse response) { // Make sure the authentication token was a parameter. This prevents // malicious code from "hijacking" the token by performing a POST and // having the browser inject it as only a cookie. if(! (Boolean) request .getAttribute( AuthFilter .ATTRIBUTE_AUTHENTICATION_TOKEN_IS_PARAM)) { throw new OmhException( "To register a third-party, the authentication token is " + "required as a parameter."); } return handleRequest( request, response, new OauthRegistrationRequest( (AuthenticationToken) request .getAttribute( AuthFilter .ATTRIBUTE_AUTHENTICATION_TOKEN), name, description, redirectUri)); } /** * <p> * The OAuth call where a user has been redirected to us by some * third-party in order for us to present them with an authorization * request, verify that the user is who they say they are, and grant or * deny the request. * </p> * * <p> * This call will either redirect the user to the authorization HTML page * with the parameters embedded or it will return a non-2xx response with a * message indicating what was wrong with the request. Unfortunately, * because the problem with the request may be that the given client ID is * unknown, we have no way to direct the user back. If we simply force the * browser to "go back", it may result in an infinite loop where the * third-party continuously redirects them back to us and visa-versa. To * avoid this, we should simply return an error string and let the user * decide. * </p> * * @param request * The HTTP request. * * @param response * The HTTP response. * * @return A OAuth-specified JSON response that indicates what was wrong * with the request. If nothing was wrong with the request, a * redirect would have been returned. * * @throws IOException * There was a problem responding to the client. * * @throws OAuthSystemException * The OAuth library encountered an error. */ @RequestMapping( value = "auth/oauth/authorize", method = { RequestMethod.GET, RequestMethod.POST }) public @ResponseBody String receiveAuthorizationCodeRequest( final HttpServletRequest request, final HttpServletResponse response) throws IOException, OAuthSystemException { // Create the OAuth request from the HTTP request. OAuthAuthzRequest oauthRequest; try { oauthRequest = new OAuthAuthzRequest(request); } // The request does not conform to the RFC, so we return a HTTP 400 // with a reason. catch(OAuthProblemException e) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .error(e) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Validate that the user is requesting a "code" response type, which // is the only response type we accept. try { if(! ResponseType .CODE.toString().equals(oauthRequest.getResponseType())) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(CodeResponse.UNSUPPORTED_RESPONSE_TYPE) .setErrorDescription( "The response type must be '" + ResponseType.CODE.toString() + "' but was instead: " + oauthRequest.getResponseType()) .setState(oauthRequest.getState()) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } } catch(IllegalArgumentException e) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(CodeResponse.UNSUPPORTED_RESPONSE_TYPE) .setErrorDescription( "The response type is unknown: " + oauthRequest.getResponseType()) .setState(oauthRequest.getState()) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Make sure no redirect URI was given. if(oauthRequest.getRedirectURI() != null) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(CodeResponse.INVALID_REQUEST) .setErrorDescription( "A URI must not be given. Instead, the one given " + "when the account was created will be used.") .setState(oauthRequest.getState()) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Attempt to get the third-party. ThirdParty thirdParty = ThirdPartyBin .getInstance().getThirdParty(oauthRequest.getClientId()); // If the third-party is unknown, reject the request. if(thirdParty == null) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(CodeResponse.INVALID_REQUEST) .setErrorDescription( "The client ID is unknown: " + oauthRequest.getClientId()) .setState(oauthRequest.getState()) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Attempt to get the scopes. Set<String> scopes = oauthRequest.getScopes(); if((scopes == null) || (scopes.size() == 0)) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(CodeResponse.INVALID_SCOPE) .setErrorDescription("A scope is required.") .setState(oauthRequest.getState()) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Validate the scopes. Registry registry = Registry.getInstance(); for(String scope : scopes) { if(registry.getSchemas(scope, null, 0, 1).size() != 1) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(CodeResponse.INVALID_SCOPE) .setErrorDescription( "Each scope must be a known schema ID: " + scope) .setState(oauthRequest.getState()) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } } // Create the temporary code to be granted or rejected by the user. AuthorizationCode code = new AuthorizationCode( thirdParty, oauthRequest.getScopes(), oauthRequest.getState()); // Store the authorization code. AuthorizationCodeBin.getInstance().storeCode(code); // Build the scope as specified by the OAuth specification. StringBuilder scopeBuilder = new StringBuilder(); for(String scope : code.getScopes()) { // Add a space unless it's the first entity. if(scopeBuilder.length() != 0) { scopeBuilder.append(' '); } // Add the scope. scopeBuilder.append(scope); } // Set the redirect. response .sendRedirect( OAuthASResponse .authorizationResponse( request, HttpServletResponse.SC_FOUND) .setCode(code.getCode()) .location("Authorize.html") .setScope(scopeBuilder.toString()) .setParam(ThirdParty.JSON_KEY_NAME, thirdParty.getName()) .setParam( ThirdParty.JSON_KEY_DESCRIPTION, thirdParty.getDescription()) .buildQueryMessage() .getLocationUri()); // Since we are redirecting the user, we don't need to return anything. return null; } /** * <p> * Handles the response from the user regarding whether or not the user * granted permission to a third-party via OAuth. If the user's credentials * are invalid or there was a general error reading the request, an error * message will be returned and displayed to the user. Once we have the * third-party's information, we will do a best-effort to redirect the user * back to the third-party with a code, which the third-party can then use * to call us later to determine the actual failure. * </p> * * @param username * The user's username. * * @param password * The user's password. * * @param granted * Whether or not the permission was granted. * * @param code * The code that was created, but not yet validated by the user. * * @param request * The HTTP request object. * * @param response * The HTTP response object. */ @RequestMapping( value = "auth/oauth/authorization", method = RequestMethod.POST) public void authenticateAuthorizationCodeRequest( @RequestParam( value = PARAM_AUTHENTICATION_USERNAME, required = true) final String username, @RequestParam( value = PARAM_AUTHENTICATION_PASSWORD, required = true) final String password, @RequestParam( value = PARAM_AUTHORIZATION_GRANTED, required = true) final boolean granted, @RequestParam( value = PARAM_AUTHORIZATION_CODE, required = false) final String code, final HttpServletRequest request, final HttpServletResponse response) throws IOException, OAuthSystemException { // Get the user. If the user's credentials are invalid for whatever // reason, an exception will be thrown and the page will echo back the // reason. User user = AuthenticationRequest.getUser(username, password); // Get the authorization code. AuthorizationCode authCode = AuthorizationCodeBin.getInstance().getCode(code); // If the code is unknown, we cannot redirect back to the third-party // because we don't know who they are. if(authCode == null) { throw new OmhException("The authorization code is unknown."); } // Verify that the code has not yet expired. if(System.currentTimeMillis() > authCode.getExpirationTime()) { response .sendRedirect( OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(CodeResponse.ACCESS_DENIED) .setErrorDescription("The code has expired.") .location( authCode .getThirdParty().getRedirectUri().toString()) .setState(authCode.getState()) .buildQueryMessage() .getLocationUri()); return; } // Get the response if it already exists. AuthorizationCodeResponse codeResponse = AuthorizationCodeResponseBin.getInstance().getResponse(code); // If the response does not exist, attempt to create a new one and // save it. if(codeResponse == null) { // Create the new code. codeResponse = new AuthorizationCodeResponse(authCode, user, granted); // Store it. AuthorizationCodeResponseBin .getInstance().storeVerification(codeResponse); } // Make sure it is being verified by the same user. else if( ! user .getUsername().equals(codeResponse.getOwner().getUsername())) { response .sendRedirect( OAuthASResponse .errorResponse(HttpServletResponse.SC_UNAUTHORIZED) .setError(CodeResponse.ACCESS_DENIED) .setErrorDescription( "The code has already been verified by another " + "user.") .location( authCode .getThirdParty().getRedirectUri().toString()) .setState(authCode.getState()) .buildQueryMessage() .getLocationUri()); } // Make sure the same grant response is being made. else if(granted == codeResponse.getGranted()) { response .sendRedirect( OAuthASResponse .errorResponse(HttpServletResponse.SC_UNAUTHORIZED) .setError(CodeResponse.ACCESS_DENIED) .setErrorDescription( "The user has re-submitted the same " + "authorization code twice with competing " + "grant values.") .location( authCode .getThirdParty().getRedirectUri().toString()) .setState(authCode.getState()) .buildQueryMessage() .getLocationUri()); } // Otherwise, this is simply a repeat of the same request as before, // and we can simply ignore it. // Redirect the user back to the third-party with the authorization // code and state. response .sendRedirect( OAuthASResponse .authorizationResponse( request, HttpServletResponse.SC_OK) .location( authCode.getThirdParty().getRedirectUri().toString()) .setCode(authCode.getCode()) .setParam("state", authCode.getState()) .buildQueryMessage() .getLocationUri()); } /** * <p> * The OAuth call when a third-party is attempting to exchange their * authorization request token for a valid authorization token. Because * this is a back-channel communication from the third-party, their ID and * secret must be given to authenticate them. They will then be returned * either an authorization token or an error message indicating what was * wrong with the request. * </p> * * @param request * The HTTP request object. * * @param response * The HTTP response object. * * @return An OAuth-specified JSON error message or an OAuth-specified JSON * response that includes the access and refresh tokens as well as * the expiration and other information. * * @throws OAuthSystemException * The OAuth library encountered an error. */ @RequestMapping( value = "auth/oauth/token", method = RequestMethod.POST) public @ResponseBody String createAuthorizationToken( final HttpServletRequest request, final HttpServletResponse response) throws OAuthSystemException, IOException { // Attempt to build an OAuth request from the HTTP request. OAuthTokenRequest oauthRequest; try { oauthRequest = new OAuthTokenRequest(request); } // If the HTTP request was not a valid OAuth token request, then we // have no other choice but to reject it as a bad request. catch(OAuthProblemException e) { // Build the OAuth response. OAuthResponse oauthResponse = OAuthResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .error(e) .buildJSONMessage(); // Set the HTTP response status code from the OAuth response. response.setStatus(oauthResponse.getResponseStatus()); // Return the error message. return oauthResponse.getBody(); } // Attempt to get the client. ThirdParty thirdParty = ThirdPartyBin .getInstance().getThirdParty(oauthRequest.getClientId()); // If the client is unknown, respond as such. if(thirdParty == null) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(TokenResponse.INVALID_CLIENT) .setErrorDescription( "The client is unknown: " + oauthRequest.getClientId()) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Get the given client secret. String thirdPartySecret = oauthRequest.getClientSecret(); if(thirdPartySecret == null) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(TokenResponse.INVALID_CLIENT) .setErrorDescription("The client secret is required.") .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Make sure the client gave the right secret. else if(! thirdPartySecret.equals(thirdParty.getSecret())) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(TokenResponse.INVALID_CLIENT) .setErrorDescription("The client secret is incorrect.") .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Get the grant-type. GrantType grantType; String grantTypeString = oauthRequest.getGrantType(); if(GrantType.AUTHORIZATION_CODE.toString().equals(grantTypeString)) { grantType = GrantType.AUTHORIZATION_CODE; } else if(GrantType.CLIENT_CREDENTIALS.toString().equals(grantTypeString)) { grantType = GrantType.CLIENT_CREDENTIALS; } else if(GrantType.PASSWORD.toString().equals(grantTypeString)) { grantType = GrantType.PASSWORD; } else if(GrantType.REFRESH_TOKEN.toString().equals(grantTypeString)) { grantType = GrantType.REFRESH_TOKEN; } else { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(TokenResponse.INVALID_GRANT) .setErrorDescription( "The grant type is unknown: " + grantTypeString) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Handle the different types of token requests. AuthorizationToken token; if(GrantType.AUTHORIZATION_CODE.equals(grantType)) { // Attempt to get the code. String codeString = oauthRequest.getCode(); if(codeString == null) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(TokenResponse.INVALID_REQUEST) .setErrorDescription( "An authorization code must be given to be " + "exchanged for an authorization token.") .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Attempt to lookup the actual AuthorizationCode object. AuthorizationCode code = AuthorizationCodeBin.getInstance().getCode(codeString); // If the code doesn't exist, reject the request. if(code == null) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(TokenResponse.INVALID_REQUEST) .setErrorDescription( "The given authorization code is unknown: " + codeString) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Verify that the client asking for a token is the same as the one // that requested the code. if(! code.getThirdParty().getId().equals(thirdParty.getId())) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(TokenResponse.INVALID_REQUEST) .setErrorDescription( "This client is not allowed to reference this " + "code: " + codeString) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // If the code has expired, reject the request. if(System.currentTimeMillis() > code.getExpirationTime()) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(TokenResponse.INVALID_REQUEST) .setErrorDescription( "The given authorization code has expired: " + codeString) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Use the code to lookup the response information and error out if // a user has not yet verified it. AuthorizationCodeResponse codeResponse = AuthorizationCodeResponseBin .getInstance().getResponse(code.getCode()); if(codeResponse == null) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(TokenResponse.INVALID_REQUEST) .setErrorDescription( "A user has not yet verified the code: " + codeString) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Determine if the user granted access and, if not, error out. if(! codeResponse.getGranted()) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(TokenResponse.INVALID_REQUEST) .setErrorDescription( "The user denied the authorization: " + codeString) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Create a new token. token = new AuthorizationToken(codeResponse); } // Handle a third-party refreshing an existing token. else if(GrantType.REFRESH_TOKEN.equals(grantType)) { // Get the refresh token from the request. String refreshToken = oauthRequest.getRefreshToken(); if(refreshToken == null) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(TokenResponse.INVALID_REQUEST) .setErrorDescription( "An refresh token must be given to be exchanged " + "for a new authorization token.") .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Use the refresh token to lookup the actual refresh token. AuthorizationToken currentToken = AuthorizationTokenBin .getInstance().getTokenFromRefreshToken(refreshToken); if(currentToken == null) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(TokenResponse.INVALID_REQUEST) .setErrorDescription("The refresh token is unknown.") .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Verify that the client asking for a token is the same as the one // that was issued the refresh token. // This is probably a very serious offense and should probably // raise some serious red flags! if(! currentToken .getThirdParty().getId().equals(thirdParty.getId())) { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(TokenResponse.INVALID_REQUEST) .setErrorDescription( "This token does not belong to this client.") .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Create a new authorization token from the current one. token = new AuthorizationToken(currentToken); } // If the grant-type is unknown, then we do not yet understand how // the request is built and, therefore, can do nothing more than // reject it via an OmhException. else { // Create the OAuth response. OAuthResponse oauthResponse = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(TokenResponse.UNSUPPORTED_GRANT_TYPE) .setErrorDescription( "The grant type must be one of '" + GrantType.AUTHORIZATION_CODE.toString() + "' or '" + GrantType.REFRESH_TOKEN.toString() + "': " + grantType.toString()) .buildJSONMessage(); // Set the status and return the error message. response.setStatus(oauthResponse.getResponseStatus()); return oauthResponse.getBody(); } // Store the new token. AuthorizationTokenBin.getInstance().storeToken(token); // Build the response. OAuthResponse oauthResponse = OAuthASResponse .tokenResponse(HttpServletResponse.SC_OK) .setAccessToken(token.getAccessToken()) .setExpiresIn(Long.valueOf(token.getExpirationIn() / 1000).toString()) .setRefreshToken(token.getRefreshToken()) .setTokenType(TokenType.BEARER.toString()) .buildJSONMessage(); // Set the status. response.setStatus(oauthResponse.getResponseStatus()); // Set the content-type. response.setContentType("application/json"); // Add the headers. Map<String, String> headers = oauthResponse.getHeaders(); for(String headerKey : headers.keySet()) { response.addHeader(headerKey, headers.get(headerKey)); } // Return the body. return oauthResponse.getBody(); } /** * <p> * Creates a new user in the database and sends an activation email. * </p> * * <p> * For systems that already have their own user activation/management * system in place, either remove this function or simply remove the * RequestMapping annotation. * </p> * * @param username * The new user's username. * * @param password * The new user's password * * @param email The new user's email address. * * @param request * The HTTP request object. * * @param response * The HTTP response object. */ @RequestMapping( value = UserRegistrationRequest.PATH, method = RequestMethod.POST) public @ResponseBody void registerUser( @RequestParam( value = User.JSON_KEY_USERNAME, required = true) final String username, @RequestParam( value = User.JSON_KEY_PASSWORD, required = true) final String password, @RequestParam( value = User.JSON_KEY_EMAIL, required = true) final String email, final HttpServletRequest request, final HttpServletResponse response) { handleRequest( request, response, new UserRegistrationRequest( username, password, email, buildRootUrl(request))); } /** * <p> * Creates a new user in the database and sends an activation email. * </p> * * <p> * For systems that already have their own user activation/management * system in place, either remove this function or simply remove the * RequestMapping annotation. * </p> * * @param username * The new user's username. * * @param password * The new user's password * * @param email The new user's email address. * * @param request * The HTTP request object. * * @param response * The HTTP response object. */ @RequestMapping( value = UserActivationRequest.PATH, method = RequestMethod.POST) public @ResponseBody void activateUser( @RequestParam( value = User.JSON_KEY_REGISTRATION_KEY, required = true) final String registrationId, final HttpServletRequest request, final HttpServletResponse response) { handleRequest( request, response, new UserActivationRequest(registrationId)); } @RequestMapping(value = "testing", method = RequestMethod.GET) public @ResponseBody User testing() { return UserBin.getInstance().getUser("sink.thaw"); } /** * If the root of the hierarchy is requested, return the registry, which is * a map of all of the schema IDs to their high-level information, e.g. * name, description, latest version, etc. * * @param numToSkip * The number of data points to skip to facilitate paging. * * @param numToReturn * The number of data points to return to facilitate paging. * * @param request * The HTTP request object. * * @param response * The HTTP response object. * * @return An array of all of the known schemas, limited by paging. */ @RequestMapping(value = { "", "/" }, method = RequestMethod.GET) public @ResponseBody MultiValueResult<String> getSchemaIds( @RequestParam( value = PARAM_PAGING_NUM_TO_SKIP, required = false, defaultValue = ListRequest.DEFAULT_NUMBER_TO_SKIP_STRING) final long numToSkip, @RequestParam( value = PARAM_PAGING_NUM_TO_RETURN, required = false, defaultValue = ListRequest.DEFAULT_NUMBER_TO_RETURN_STRING) final long numToReturn, final HttpServletRequest request, final HttpServletResponse response) { return handleRequest( request, response, new SchemaIdsRequest(numToSkip, numToReturn)); } /** * Creates a request to get the information about the given schema ID, e.g. * the name, description, version list, etc. * * @param schemaId * The schema ID from the URL. * * @param numToSkip * The number of data points to skip to facilitate paging. * * @param numToReturn * The number of data points to return to facilitate paging. * * @param response * The HTTP response object. * * @return An array of schemas, one for each version of the given schema * ID. */ @RequestMapping( value = "{" + PARAM_SCHEMA_ID + "}", method = RequestMethod.GET) public @ResponseBody MultiValueResult<Long> getSchemaVersions( @PathVariable(PARAM_SCHEMA_ID) final String schemaId, @RequestParam( value = PARAM_PAGING_NUM_TO_SKIP, required = false, defaultValue = ListRequest.DEFAULT_NUMBER_TO_SKIP_STRING) final long numToSkip, @RequestParam( value = PARAM_PAGING_NUM_TO_RETURN, required = false, defaultValue = ListRequest.DEFAULT_NUMBER_TO_RETURN_STRING) final long numToReturn, final HttpServletRequest request, final HttpServletResponse response) { return handleRequest( request, response, new SchemaVersionsRequest(schemaId, numToSkip, numToReturn)); } /** * Creates a request to get the definition of a specific schema ID's * version. * * @param schemaId * The schema ID from the URL. * * @param version * The schema version from the URL. * * @param response * The HTTP response object. * * @return The schema for the given schema ID-version pair. */ @RequestMapping( value = "{" + PARAM_SCHEMA_ID + "}/" + "{" + PARAM_SCHEMA_VERSION + ":[\\d]" + "}", method = RequestMethod.GET) public @ResponseBody Object getDefinition( @PathVariable(PARAM_SCHEMA_ID) final String schemaId, @PathVariable(PARAM_SCHEMA_VERSION) final Long version, final HttpServletRequest request, final HttpServletResponse response) { return handleRequest( request, response, new SchemaRequest(schemaId, version)); } /** * Retrieves the requested data. * * @param schemaId * The ID for the schema to which the data pertains. This is part of * the request's path. * * @param version * The version of the schema to which the data pertains. This is * part of the request's path. * * @param owner * The user that owns the desired data. * * @param columnList * The list of columns to return to the user. * * @param numToSkip * The number of data points to skip to facilitate paging. * * @param numToReturn * The number of data points to return to facilitate paging. * * @param request * The HTTP request object. * * @param response * The HTTP response object. * * @return The data as a JSON array of JSON objects where each object * represents a single data point. * * @see Data */ @RequestMapping( value = "{" + PARAM_SCHEMA_ID + "}/{" + PARAM_SCHEMA_VERSION + "}/data", method = RequestMethod.GET) public @ResponseBody MultiValueResult<Data> getData( @PathVariable(PARAM_SCHEMA_ID) final String schemaId, @PathVariable(PARAM_SCHEMA_VERSION) final Long version, @RequestParam( value = PARAM_OWNER, required = false) final String owner, @RequestParam( value = PARAM_COLUMN_LIST, required = false) final List<String> columnList, @RequestParam( value = PARAM_PAGING_NUM_TO_SKIP, required = false, defaultValue = ListRequest.DEFAULT_NUMBER_TO_SKIP_STRING) final long numToSkip, @RequestParam( value = PARAM_PAGING_NUM_TO_RETURN, required = false, defaultValue = ListRequest.DEFAULT_NUMBER_TO_RETURN_STRING) final long numToReturn, final HttpServletRequest request, final HttpServletResponse response) { // Handle the request. return handleRequest( request, response, new DataReadRequest( (AuthenticationToken) request .getAttribute( AuthFilter .ATTRIBUTE_AUTHENTICATION_TOKEN), (AuthorizationToken) request .getAttribute( AuthFilter .ATTRIBUTE_AUTHORIZATION_TOKEN), schemaId, version, owner, columnList, numToSkip, numToReturn)); } /** * Writes the requested data. * * @param schemaId * The ID for the schema to which the data pertains. * * @param version * The version of the schema to which the data pertains. * * @param data * The data to be uploaded, which should be a JSON array of JSON * objects where each object is a single data point. * * @param request * The HTTP request object. * * @param response * The HTTP response object. */ @RequestMapping( value = "{" + PARAM_SCHEMA_ID + "}/{" + PARAM_SCHEMA_VERSION + "}/data", method = RequestMethod.POST) public void putData( @PathVariable(PARAM_SCHEMA_ID) final String schemaId, @PathVariable(PARAM_SCHEMA_VERSION) final Long version, @RequestParam( value = PARAM_DATA, required = true) final String data, final HttpServletRequest request, final HttpServletResponse response) { // Make sure the authentication token was a parameter. This prevents // malicious code from "hijacking" the token by performing a POST and // having the browser inject it as only a cookie. Object authenticationTokenIsParam = request .getAttribute( AuthFilter.ATTRIBUTE_AUTHENTICATION_TOKEN_IS_PARAM); if( (authenticationTokenIsParam == null) || (! ((Boolean) authenticationTokenIsParam))) { throw new OmhException( "To upload data, the authentication token is required " + "as a parameter."); } // Get the authentication token. AuthenticationToken authToken = (AuthenticationToken) request .getAttribute( AuthFilter.ATTRIBUTE_AUTHENTICATION_TOKEN); // Handle the request. handleRequest( request, response, new DataWriteRequest( authToken, schemaId, version, data)); } /** * Handles a request then sets the meta-data as HTTP headers and returns * the data to be returned to the user. * * @param httpRequest * The HTTP request. * * @param httpResponse * The HTTP response. * * @param request * The already-built, domain-specific request to be serviced. * * @return The object to be returned to the user. */ private <T> T handleRequest( final HttpServletRequest httpRequest, final HttpServletResponse httpResponse, final Request<? extends T> request) { // Service the request. request.service(); // Retrieve the meta-data and add it as HTTP headers. Map<String, Object> metaData = request.getMetaData(); if(metaData != null) { for(String metaDataKey : metaData.keySet()) { httpResponse .setHeader( metaDataKey, metaData.get(metaDataKey).toString()); } } // If this is a list request, add the next and previous parameters. if(request instanceof ListRequest) { // Create the previous and next headers, if appropriate. addNextPreviousHeaders( httpRequest, httpResponse, (ListRequest<?>) request); } // Return the data. return request.getData(); } /** * Builds the base URL for the request that came in. This is everything up * to our web applications base, e.g. "http://localhost:8080/omh". * * @param httpRequest * The original HTTP request. * * @return The base URL for the request. */ private String buildRootUrl(final HttpServletRequest httpRequest) { // It must be a HTTP request. StringBuilder builder = new StringBuilder("http"); // If security was used add the "s" to make it "https". if(httpRequest.isSecure()) { builder.append('s'); } // Add the protocol separator. builder.append("://"); // Add the name of the server where the request was sent. builder.append(httpRequest.getServerName()); // Add the port separator and the port. builder.append(':').append(httpRequest.getServerPort()); // Add the context path, e.g. "/omh". builder.append(httpRequest.getContextPath()); // Return the root URL. return builder.toString(); } /** * <p> * Builds the URL used to make this request based on the request. * </p> * * <p> * The URL is built from all of the information Java provides about the * system including the hostname. However, in a distributed environment, * this may not be adequate or correct. * </p> * * @param httpRequest * The original HTTP request. * * @return The base URL used to make the request that is calling this * function. */ private String buildRequestUrl(final HttpServletRequest httpRequest) { // Start with the root URL. StringBuilder builder = new StringBuilder(buildRootUrl(httpRequest)); // Add the specific path in the request. builder.append(httpRequest.getPathInfo()); // Return the base URL, which should only need to have the parameters // added to it. return builder.toString(); } /** * Creates and adds the Previous and Next headers. * * @param httpRequest * The HTTP request. * * @param httpResponse * The HTTP response. * * @param listRequest * The ListRequest used to get the paging headers. */ private void addNextPreviousHeaders( final HttpServletRequest httpRequest, final HttpServletResponse httpResponse, final ListRequest<?> listRequest) { // Get the new set of parameters. Map<String, String> parameters = listRequest.getPreviousNextParameters(); // If we skipped any data, create a Previous header. if(listRequest.getNumToSkip() > 0) { // Build the base URL. StringBuilder previousBuilder = new StringBuilder(buildRequestUrl(httpRequest)); // Add the query separator. previousBuilder.append('?'); // Use a try-catch in case our encoding, which is the same for // each parameter, is unknown. try { // Add each of the custom parameters. boolean firstPass = true; for(String parameterKey : parameters.keySet()) { // Add the parameter separator. if(firstPass) { firstPass = false; } else { previousBuilder.append('&'); } // Add the parameter. previousBuilder .append( URLEncoder .encode(parameterKey, URL_ENCODING_UTF_8)) .append('=') .append( URLEncoder .encode( parameters.get(parameterKey), URL_ENCODING_UTF_8)); } // Add the paging parameters. if(parameters.size() > 0) { previousBuilder.append('&'); } // Calculate the previous number to skip. long previousNumToSkip = listRequest.getNumToSkip() - listRequest.getNumToReturn(); // If the previous number to skip is greater than zero, add // the number to skip. if(previousNumToSkip > 0) { previousBuilder .append( URLEncoder .encode( PARAM_PAGING_NUM_TO_SKIP, URL_ENCODING_UTF_8)) .append('=') .append( URLEncoder .encode( Long.toString(previousNumToSkip), URL_ENCODING_UTF_8)); previousBuilder.append('&'); } // Always add the number to return. previousBuilder .append( URLEncoder .encode( PARAM_PAGING_NUM_TO_RETURN, URL_ENCODING_UTF_8)) .append('=') .append( URLEncoder .encode( Long.toString( Math.min( listRequest.getNumToSkip(), listRequest.getNumToReturn())), URL_ENCODING_UTF_8)); } catch(UnsupportedEncodingException e) { LOGGER .log( Level.SEVERE, "The encoding is unknown so the " + HEADER_PREVIOUS + " header could not be built."); } // Add the previous header. httpResponse .setHeader(HEADER_PREVIOUS, previousBuilder.toString()); } // If the total data-set size is greater than the number of points // skipped plus the number of points requested, then there must be more // data, and a Next header should be added. if(listRequest.getData().count() > (listRequest.getNumToSkip() + listRequest.getNumToReturn())) { // Build the base URL. StringBuilder nextBuilder = new StringBuilder(buildRequestUrl(httpRequest)); // Add the query separator. nextBuilder.append('?'); // Use a try-catch in case our encoding, which is the same for // each parameter, is unknown. try { // Add each of the custom parameters. boolean firstPass = true; for(String parameterKey : parameters.keySet()) { // Add the parameter separator. if(firstPass) { firstPass = false; } else { nextBuilder.append('&'); } // Add the parameter. nextBuilder .append( URLEncoder .encode(parameterKey, URL_ENCODING_UTF_8)) .append('=') .append( URLEncoder .encode( parameters.get(parameterKey), URL_ENCODING_UTF_8)); } // Add the paging parameters. if(parameters.size() > 0) { nextBuilder.append('&'); } // Calculate the previous number to skip. long nextNumToSkip = listRequest.getNumToSkip() + listRequest.getNumToReturn(); // Always add the number to skip. nextBuilder .append( URLEncoder .encode( PARAM_PAGING_NUM_TO_SKIP, URL_ENCODING_UTF_8)) .append('=') .append( URLEncoder .encode( Long.toString(nextNumToSkip), URL_ENCODING_UTF_8)); // Add the parameter separator. nextBuilder.append('&'); // Always add the number to return. nextBuilder .append( URLEncoder .encode( PARAM_PAGING_NUM_TO_RETURN, URL_ENCODING_UTF_8)) .append('=') .append( URLEncoder .encode( Long.toString(listRequest.getNumToReturn()), URL_ENCODING_UTF_8)); } catch(UnsupportedEncodingException e) { LOGGER .log( Level.SEVERE, "The encoding is unknown so the " + HEADER_NEXT + " header could not be built."); } // Add the previous header. httpResponse .setHeader(HEADER_NEXT, nextBuilder.toString()); } } }