/* * Copyright 2012 Janrain, Inc. * * 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.janrain.backplane2.server; import com.janrain.backplane.common.AuthException; import com.janrain.backplane.common.BackplaneServerException; import com.janrain.backplane.common.DateTimeUtils; import com.janrain.backplane.config.BackplaneConfig; import com.janrain.backplane.dao.DaoException; import com.janrain.backplane.server2.MessageResponse; import com.janrain.backplane.server2.dao.BP2DAOs; import com.janrain.backplane.server2.model.*; import com.janrain.backplane.server2.model.Channel; import com.janrain.backplane.server2.oauth2.model.*; import com.janrain.backplane.server2.oauth2.model.Token; import com.janrain.backplane.server2.model.Backplane2Message; import com.janrain.commons.supersimpledb.SimpleDBException; import com.janrain.oauth2.*; import com.janrain.servlet.InvalidRequestException; import com.janrain.util.RandomUtils; import com.janrain.util.ServletUtil; import com.janrain.utils.AnalyticsLogger; import com.yammer.metrics.core.MetricName; import com.yammer.metrics.core.TimerContext; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.codehaus.jackson.map.ObjectMapper; import org.springframework.stereotype.Controller; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import scala.Option; import scala.Tuple2; import scala.collection.JavaConversions; import javax.inject.Inject; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.UnsupportedEncodingException; import java.util.*; import java.util.concurrent.TimeUnit; import static com.janrain.oauth2.OAuth2.*; import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; /** * Backplane API implementation. * * @author Johnny Bufu, Tom Raney */ @Controller @RequestMapping(value="/v2/*") @SuppressWarnings({"UnusedDeclaration"}) public class Backplane2Controller { // - PUBLIC /** both view name and jsp variable */ public static final String DIRECT_RESPONSE = "direct_response"; /** * Handle dynamic discovery of this server's registration endpoint * @return */ @RequestMapping(value = "/.well-known/host-meta", method = { RequestMethod.GET}) public ModelAndView xrds(HttpServletRequest request, HttpServletResponse response) { ModelAndView view = new ModelAndView("xrd"); view.addObject("host", "http://" + request.getServerName()); view.addObject("secureHost", "https://" + request.getServerName()); return view; } @RequestMapping(value = "/authorize", method = { RequestMethod.GET, RequestMethod.POST }) public ModelAndView authorize( HttpServletRequest request, HttpServletResponse response, @CookieValue( value = AUTH_SESSION_COOKIE, required = false) String authSessionCookie, @CookieValue( value = AUTHORIZATION_REQUEST_COOKIE, required = false) String authorizationRequestCookie) throws AuthorizationException, DaoException { AuthorizationRequest authzRequest = null; String httpMethod = request.getMethod(); String authZdecisionKey = request.getParameter(AUTHZ_DECISION_KEY); if (authZdecisionKey != null) { logger.debug("received valid authZdecisionKey:" + authZdecisionKey); } // not return from /authenticate && not authz decision post if ( request.getParameterMap().size() > 0 && StringUtils.isEmpty(authZdecisionKey) ) { // incoming authz request authzRequest = parseAuthZrequest(request); } String authenticatedBusOwner = getAuthenticatedBusOwner(request, authSessionCookie); if (null == authenticatedBusOwner) { if (null != authzRequest) { try { logger.info("Persisting authorization request for client: " + authzRequest.get(AuthorizationRequestFields.CLIENT_ID()) + "[" + authzRequest.get(AuthorizationRequestFields.COOKIE())+"]"); com.janrain.backplane.server2.dao.BP2DAOs.authorizationRequestDao().store(authzRequest); response.addCookie(new Cookie(AUTHORIZATION_REQUEST_COOKIE, authzRequest.get(AuthorizationRequestFields.COOKIE()).get())); } catch (Exception e) { throw new AuthorizationException(OAuth2.OAUTH2_AUTHZ_SERVER_ERROR, e.getMessage(), request, e); } } logger.info("Bus owner not authenticated, redirecting to /authenticate"); return new ModelAndView("redirect:https://" + request.getServerName() + "/v2/authenticate"); } if (StringUtils.isEmpty(authZdecisionKey)) { // authorization request if (null == authzRequest) { // return from /authenticate try { logger.debug("bp2.authorization.request cookie = " + authorizationRequestCookie); authzRequest = com.janrain.backplane.server2.dao.BP2DAOs.authorizationRequestDao().get(authorizationRequestCookie).get(); logger.info("Retrieved authorization request for client:" + authzRequest.get(AuthorizationRequestFields.CLIENT_ID()) + "[" + authzRequest.get(AuthorizationRequestFields.COOKIE())+"]"); } catch (Exception e) { throw new AuthorizationException(OAuth2.OAUTH2_AUTHZ_SERVER_ERROR, e.getMessage(), request, e); } } return processAuthZrequest(authzRequest, authSessionCookie, authenticatedBusOwner); } else { // authZ decision from bus owner, accept only on post if (! "POST".equals(httpMethod)) { throw new InvalidRequestException("Invalid HTTP method for authorization decision post: " + httpMethod); } return processAuthZdecision(authZdecisionKey, authSessionCookie, authenticatedBusOwner, authorizationRequestCookie, request); } } /** * Authenticates a bus owner and stores the authenticated session (cookie) to simpleDB. * * GET: displays authentication form * POST: processes authentication and returns to /authorize */ @RequestMapping(value = "/authenticate", method = { RequestMethod.GET, RequestMethod.POST }) public ModelAndView authenticate( HttpServletRequest request, HttpServletResponse response, @RequestParam(required = false) String busOwner, @RequestParam(required = false) String password) throws AuthException, BackplaneServerException, DaoException { ServletUtil.checkSecure(request); String httpMethod = request.getMethod(); if ("GET".equals(httpMethod)) { logger.debug("returning view for GET"); return new ModelAndView(BUS_OWNER_AUTH_FORM_JSP); } else if ("POST".equals(httpMethod)) { BP2DAOs.busOwnerDao().getAuthenticated(busOwner, password); logger.info("Authenticated bus owner: " + busOwner); persistAuthenticatedSession(response, busOwner); return new ModelAndView("redirect:https://" + request.getServerName() + "/v2/authorize"); } else { throw new InvalidRequestException("Unsupported method for /authenticate: " + httpMethod); } } /** * The OAuth "Token Endpoint" is used to obtain an access token to be used * for retrieving messages from the Get Messages endpoint. * * @param scope optional * @param callback required * @return * @throws AuthException * @throws SimpleDBException * @throws BackplaneServerException */ @RequestMapping(value = "/token", method = { RequestMethod.GET}) @ResponseBody public Map<String,Object> getToken(final HttpServletRequest request, HttpServletResponse response, @RequestParam(value = OAUTH2_SCOPE_PARAM_NAME, required = false) final String scope, @RequestParam(required = false) final String bus, @RequestParam(required = false) final String callback, @RequestHeader(value = "Referer", required = false) String referer) throws DaoException { ServletUtil.checkSecure(request); final TimerContext context = getRegularTokenTimer.time(); try { Map<String,Object> result = new AnonymousTokenRequest(callback, bus, scope, request).tokenResponse(); // Refresh token requests are not logged to analytics. if (StringUtils.isNotEmpty(bus)) { aniLogNewChannel(request, referer, bus, (String) result.get(OAUTH2_SCOPE_PARAM_NAME)); } return result; } catch (TokenException e) { return handleTokenException(e, response); } finally { context.stop(); } } /** * The OAuth "Token Endpoint" is used to obtain an access token to be used * for retrieving messages from the Get Messages endpoint. * * @param client_id * @param grant_type * @param redirect_uri * @param code * @param client_secret * @param scope * @return * @throws AuthException * @throws SimpleDBException * @throws BackplaneServerException */ @RequestMapping(value = "/token", method = { RequestMethod.POST}) @ResponseBody public Map<String,Object> token(HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "client_id", required = false) String client_id, @RequestParam(value = "grant_type", required = false) String grant_type, @RequestParam(value = "redirect_uri", required = false) String redirect_uri, @RequestParam(value = "code", required = false) String code, @RequestParam(value = "client_secret", required = false) String client_secret, @RequestParam(value = "scope", required = false) String scope, @RequestHeader(value = "Authorization", required = false) String authorizationHeader) { ServletUtil.checkSecure(request); TimerContext context = getPrivilegedTokenTimer.time(); try { checkClientCredentialsBasicAuthOnly(request.getQueryString(), client_id, client_secret); Client authenticatedClient = getAuthenticatedClient(authorizationHeader); return (new AuthenticatedTokenRequest( grant_type, authenticatedClient, code, redirect_uri, scope, request)).tokenResponse(); } catch (TokenException e) { return handleTokenException(e, response); } catch (AuthException e) { logger.error(e.getMessage()); return handleTokenException(new TokenException(OAUTH2_TOKEN_INVALID_CLIENT, "Client authentication failed", SC_UNAUTHORIZED, e), response); } catch (Exception e) { return handleTokenException(new TokenException(OAUTH2_TOKEN_SERVER_ERROR, e.getMessage(), HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e), response); } finally { context.stop(); } } /** * Retrieve messages from the server. * * @param access_token required * @param block optional * @param callback optional * @param since optional * @return json object * @throws SimpleDBException * @throws BackplaneServerException */ @RequestMapping(value = "/messages", method = { RequestMethod.GET}) public @ResponseBody Map<String,Object> messages(final HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "block", defaultValue = "0", required = false) String block, @RequestParam(required = false) String callback, @RequestParam(value = "since", required = false) String since, @RequestHeader(value = "Referer", required = false) String referer) throws SimpleDBException, BackplaneServerException { ServletUtil.checkSecure(request); TimerContext context = null; // only time the event if it is not blocking if ("0".equals(block)) { context = v2GetsTimer.time(); } try { MessageRequest messageRequest = new MessageRequest(callback, since, block); Option<Token> token = Token.fromRequest(request); if ( ! token.isDefined()) { throw new TokenException("invalid token", HttpServletResponse.SC_FORBIDDEN); } if (token.get().grantType().isRefresh()) { return returnMessage(OAuth2.OAUTH2_TOKEN_INVALID_REQUEST, "Invalid token type: " + token.get().grantType(), HttpServletResponse.SC_FORBIDDEN, response); } // val (framesResult, messages) = ... Tuple2<Map<String,Object>,scala.collection.immutable.List<Backplane2Message>> framesResultAndMessages = MessageResponse.scalaObject().apply( request.getServerName(), token.get().grantType().isPrivileged(), token.get().scope(), messageRequest.getSince(), MESSAGES_POLL_SLEEP_MILLIS, messageRequest.getReturnBefore()); aniLogPollMessages(request, referer, JavaConversions.asJavaList(framesResultAndMessages._2())); return framesResultAndMessages._1(); } catch (TokenException te) { return handleTokenException(te, response); } catch (InvalidRequestException ire) { throw ire; } catch (Exception e) { throw new BackplaneServerException("Error processing messages request: " + e.getMessage(), e); } finally { if (context != null) { context.stop(); } } } /* public Map<String, Object> asResponseFields(String serverName, boolean privileged) throws BackplaneServerException { List<Map<String,Object>> frames = new ArrayList<Map<String, Object>>(); for (Backplane2Message message : messages) { frames.add(message.asFrame(serverName, privileged)); } Map<String, Object> messagesResponse = new HashMap<String, Object>(); messagesResponse.put("nextURL", "https://" + serverName + "/v2/messages" + (!StringUtils.isBlank(lastMessageId) ? "?since=" + lastMessageId : "")); messagesResponse.put("moreMessages", moreMessages); messagesResponse.put("messages", frames); return messagesResponse; } */ /** * Retrieve a single message from the server. * * @param request * @param response * @return */ @RequestMapping(value = "/message/{msg_id:.*}", method = { RequestMethod.GET}) public @ResponseBody Map<String,Object> message(HttpServletRequest request, HttpServletResponse response, @PathVariable final String msg_id, @RequestParam(required = false) String callback) throws BackplaneServerException, SimpleDBException, DaoException { ServletUtil.checkSecure(request); TimerContext context = v2GetSingleMessageTimer.time(); try { new MessageRequest(callback, null, "0"); // validate callback only, if present } catch (InvalidRequestException e) { return handleInvalidRequest(e, response); } try { Option<Token> token = Token.fromRequest(request); if ( ! token.isDefined()) { throw new TokenException("invalid token", HttpServletResponse.SC_FORBIDDEN); } if (token.get().grantType().isRefresh()) { throw new TokenException("Invalid token type: " + token.get().grantType(), HttpServletResponse.SC_FORBIDDEN); } Backplane2Message message = BP2DAOs.messageDao().get(msg_id).getOrElse(null); if (message != null) { if (! token.get().scope().isMessageInScope(message)) { throw new TokenException("Message id '" + msg_id + "' not found", HttpServletResponse.SC_NOT_FOUND); } Map<String,Object> result = JavaConversions.mapAsJavaMap(message.asFrame(request.getServerName(), token.get().grantType().isPrivileged())); aniLogGetMessage(request, message, token.get()); return result; } else { return returnMessage(OAuth2.OAUTH2_TOKEN_INVALID_REQUEST, "Message id '" + msg_id + "' not found", HttpServletResponse.SC_NOT_FOUND, response); } } catch (TokenException te) { return handleTokenException(te, response); } finally { context.stop(); } } /** * Publish message to Backplane. * @param request * @param response * @return */ @RequestMapping(value = "/message", method = { RequestMethod.POST}) public @ResponseBody Map<String,Object> postMessages( HttpServletRequest request, HttpServletResponse response, @RequestBody Map<String,Map<String,Object>> messagePostBody) throws SimpleDBException, BackplaneServerException { ServletUtil.checkSecure(request); final TimerContext context = v2PostTimer.time(); try { Option<Token> token = Token.fromRequest(request); if ( ! token.isDefined()) { throw new TokenException("invalid token", HttpServletResponse.SC_FORBIDDEN); } if ( token.get().grantType().isRefresh() || ! token.get().grantType().isPrivileged() ) { throw new TokenException("Invalid token type: " + token.get().grantType(), HttpServletResponse.SC_FORBIDDEN); } Backplane2Message message = parsePostedMessage(messagePostBody, token.get()); BP2DAOs.messageDao().store(message); aniLogNewMessage(request, message, token.get()); response.setStatus(HttpServletResponse.SC_CREATED); return null; } catch (TokenException te) { return handleTokenException(te, response); } catch (InvalidRequestException ire) { throw ire; } catch (Exception e) { throw new BackplaneServerException("Error processing post request: " + e.getMessage(), e); } finally { context.stop(); } } public Map<String, Object> returnMessage(final String errorCode, final String errorMessage, int responseCode, HttpServletResponse response) { response.setStatus(responseCode); return new HashMap<String,Object>() {{ put(ERR_MSG_FIELD, errorCode); put(ERR_MSG_DESCRIPTION, errorMessage); }}; } @ExceptionHandler public ModelAndView handleOauthAuthzError(final AuthorizationException e) { return authzRequestError(e.getOauthErrorCode(), e.getMessage(), e.getRedirectUri(), e.getState()); } @ExceptionHandler @ResponseBody public Map<String,Object> handleTokenException(final TokenException e, HttpServletResponse response) { logger.warn("Error processing token request: " + e.getMessage(), BackplaneConfig.getDebugException(e)); response.setStatus(e.getHttpResponseCode()); return new HashMap<String,Object>() {{ put(ERR_MSG_FIELD, e.getOauthErrorCode()); put(ERR_MSG_DESCRIPTION, e.getMessage()); }}; } /** * Handle auth errors as part of normal application flow */ @ExceptionHandler @ResponseBody public Map<String, String> handle(final AuthException e, HttpServletResponse response) { logger.warn("Backplane authentication error: " + e.getMessage(), BackplaneConfig.getDebugException(e)); response.setStatus(SC_UNAUTHORIZED); return new HashMap<String,String>() {{ put(ERR_MSG_FIELD, e.getMessage()); }}; } /** * Handle invalid requests as a normal part of application flow */ @ExceptionHandler @ResponseBody public Map<String, Object> handleInvalidRequest(final InvalidRequestException e, HttpServletResponse response) { logger.error("Error handling backplane request: " + e.getMessage(), BackplaneConfig.getDebugException(e)); response.setStatus(e.getHttpResponseCode()); return new HashMap<String,Object>() {{ put(ERR_MSG_FIELD, e.getMessage()); String errorDescription = e.getErrorDescription(); if (StringUtils.isNotEmpty(errorDescription)) { put(ERR_MSG_DESCRIPTION, errorDescription); } }}; } /** * Handle invalid HTTP request method exceptions */ @ExceptionHandler @ResponseBody public Map<String, Object> handleInvalidRequest(final HttpRequestMethodNotSupportedException e, HttpServletResponse response) { logger.warn("Error handling backplane request: " + e.getMessage(), BackplaneConfig.getDebugException(e)); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return new HashMap<String,Object>() {{ put(ERR_MSG_FIELD, e.getMessage()); }}; } /** * Handle all other errors not normally a part of application flow. */ @ExceptionHandler @ResponseBody public Map<String, String> handle(final Exception e, HttpServletResponse response) { logger.error("Error handling backplane request: " + e.getMessage(), BackplaneConfig.getDebugException(e)); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return new HashMap<String,String>() {{ put(ERR_MSG_FIELD, BackplaneConfig.isDebugMode() ? e.getMessage() : "Error processing request."); }}; } /* public static String randomString(int length) { byte[] randomBytes = new byte[length]; // the base64 character set per RFC 4648 with last two members '-' and '_' removed due to possible // compatibility issues. byte[] digits = {'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T', 'U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n', 'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7', '8','9'}; random.nextBytes(randomBytes); for (int i = 0; i < length; i++) { byte b = randomBytes[i]; int c = Math.abs(b % digits.length); randomBytes[i] = digits[c]; } try { return new String(randomBytes, "US-ASCII"); } catch (UnsupportedEncodingException e) { logger.error("US-ASCII character encoding not supported", e); // shouldn't happen return null; } } */ // - PRIVATE private static final Logger logger = Logger.getLogger(Backplane2Controller.class); private static final String ERR_MSG_FIELD = "error"; private static final String ERR_MSG_DESCRIPTION = "error_description"; private static final String BUS_OWNER_AUTH_FORM_JSP = "bus_owner_auth"; private static final String CLIENT_AUTHORIZATION_FORM_JSP = "client_authorization"; private static final int AUTH_SESSION_COOKIE_LENGTH = 30; private static final String AUTH_SESSION_COOKIE = "bp2.bus.owner.auth"; private static final int AUTHORIZATION_REQUEST_COOKIE_LENGTH = 30; private static final String AUTHORIZATION_REQUEST_COOKIE = "bp2.authorization.request"; public static final String AUTHZ_DECISION_KEY = "auth_key"; private static final int MESSAGES_POLL_SLEEP_MILLIS = 3000; @Inject private AnalyticsLogger anilogger; private void persistAuthenticatedSession(HttpServletResponse response, String busOwner) throws BackplaneServerException, DaoException { try { String authCookie = RandomUtils.randomString(AUTH_SESSION_COOKIE_LENGTH); com.janrain.backplane.server2.dao.BP2DAOs.authSessionDao().store(new AuthSession(busOwner, authCookie)); response.addCookie(new Cookie(AUTH_SESSION_COOKIE, authCookie)); } catch (Exception e) { throw new BackplaneServerException(e.getMessage()); } } private String getAuthenticatedBusOwner(HttpServletRequest request, String authSessionCookie) throws DaoException { if (authSessionCookie == null) return null; try { AuthSession authSession = com.janrain.backplane.server2.dao.BP2DAOs.authSessionDao().get(authSessionCookie).getOrElse(null); if (authSession == null) { return null; } else { String authenticatedOwner = authSession.get(AuthSessionFields.AUTH_USER()).get(); logger.info("Session found for previously authenticated bus owner: " + authenticatedOwner); return authenticatedOwner; } } catch (Exception e) { logger.error("Error looking up session for cookie: " + authSessionCookie, e); return null; } } private void authError(String errMsg) throws AuthException { logger.error(errMsg); try { throw new AuthException("Access denied. " + (BackplaneConfig.isDebugMode() ? errMsg : "")); } catch (Exception e) { throw new AuthException("Access denied."); } } private String paddedResponse(String callback, String s) { if (StringUtils.isBlank(callback)) { throw new InvalidRequestException("Callback cannot be blank."); } StringBuilder result = new StringBuilder(callback); result.append("(").append(s).append(")"); return result.toString(); } /** Parse, extract & validate an OAuth2 authorization request from the HTTP request */ private AuthorizationRequest parseAuthZrequest(HttpServletRequest request) throws AuthorizationException { try { // parse authz request AuthorizationRequest authorizationRequest = new AuthorizationRequest( RandomUtils.randomString(AUTHORIZATION_REQUEST_COOKIE_LENGTH), ServletUtil.singleValueRequestParams(request)); logger.info("Parsed authorization request: " + authorizationRequest); return authorizationRequest; } catch (Exception e) { throw new AuthorizationException(OAuth2.OAUTH2_AUTHZ_INVALID_REQUEST, e.getMessage(), request, e); } } /** Returns an authenticated Client, never null, or throws AuthException */ private Client getAuthenticatedClient(String basicAuth) throws AuthException { String userPass = null; if (basicAuth == null || !basicAuth.startsWith("Basic ") || basicAuth.length() < 7) { authError("Invalid client authorization header: " + basicAuth); } else { try { userPass = new String(Base64.decodeBase64(basicAuth.substring(6).getBytes("utf-8"))); } catch (UnsupportedEncodingException e) { authError("Cannot check client authentication, unsupported encoding: utf-8"); // shouldn't happen } } @SuppressWarnings({"ConstantConditions"}) int delim = userPass.indexOf(":"); if (delim == -1) { authError("Invalid Basic auth token: " + userPass); } String client = userPass.substring(0, delim); String pass = userPass.substring(delim + 1); return BP2DAOs.clientDao().getAuthenticated(client, pass); } /** Present an authorization form to the bus owner and obtain authorization decision */ private ModelAndView processAuthZrequest(AuthorizationRequest authzRequest, String authSessionCookie, String authenticatedBusOwner) throws AuthorizationException { Map<String,String> model = new HashMap<String, String>(); // generate & persist authZdecisionKey logger.debug("generate & persist authZdecisionKey"); try { AuthorizationDecisionKey authorizationDecisionKey = new AuthorizationDecisionKey(authSessionCookie); com.janrain.backplane.server2.dao.BP2DAOs.authorizationDecisionKeyDao().store(authorizationDecisionKey); model.put("auth_key", authorizationDecisionKey.get(AuthorizationDecisionKeyFields.KEY()).get()); model.put(AuthorizationRequestFields.CLIENT_ID().name().toLowerCase(), (String) authzRequest.get(AuthorizationRequestFields.CLIENT_ID().name()).getOrElse(null)); model.put(AuthorizationRequestFields.REDIRECT_URI().name().toLowerCase(), authzRequest.getRedirectUri()); Option<String> scopeOption = authzRequest.get(AuthorizationRequestFields.SCOPE().name()); String scope = scopeOption.isDefined() ? scopeOption.get() : null; model.put(AuthorizationRequestFields.SCOPE().name().toLowerCase(), checkScope(scope, authenticatedBusOwner) ); // return authZ form logger.info("Requesting bus owner authorization for :" + authzRequest.get(AuthorizationRequestFields.CLIENT_ID()) + "[" + authzRequest.get(AuthorizationRequestFields.COOKIE())+"]"); return new ModelAndView(CLIENT_AUTHORIZATION_FORM_JSP, model); } catch (Exception e) { throw new AuthorizationException(OAuth2.OAUTH2_AUTHZ_SERVER_ERROR, e.getMessage(), authzRequest, e); } } private String checkScope(String scope, String authenticatedBusOwner) throws BackplaneServerException { StringBuilder result = new StringBuilder(); List<BusConfig2> ownedBuses = JavaConversions.seqAsJavaList(BP2DAOs.busDao().retrieveByOwner(authenticatedBusOwner)); if(StringUtils.isEmpty(scope)) { // request scope empty, ask/offer permission to all owned buses for(BusConfig2 bus : ownedBuses) { result.append("bus:").append(bus.get(BusConfig2Fields.BUS_NAME())).append(" "); } if(result.length() > 0) { result.deleteCharAt(result.length()-1); } } else { List<String> ownedBusNames = new ArrayList<String>(); for(BusConfig2 bus : ownedBuses) { ownedBusNames.add(bus.id()); } for(String scopeToken : scope.split(" ")) { if(scopeToken.startsWith("bus:")) { String bus = scopeToken.substring(4); if (! ownedBusNames.contains(bus) ) continue; } result.append(scopeToken).append(" "); } if(result.length() > 0) { result.deleteCharAt(result.length()-1); } } String resultString = result.toString(); if (! resultString.equals(scope)) { logger.info("Authenticated bus owner " + authenticatedBusOwner + " is authoritative for requested scope: " + resultString); } return resultString; } private ModelAndView processAuthZdecision(String authZdecisionKey, String authSessionCookie, String authenticatedBusOwner, String authorizationRequestCookie, HttpServletRequest request) throws AuthorizationException { AuthorizationRequest authorizationRequest = null; logger.debug("processAuthZdecision()"); try { // retrieve authorization request authorizationRequest = com.janrain.backplane.server2.dao.BP2DAOs.authorizationRequestDao().get(authorizationRequestCookie).get(); // check authZdecisionKey AuthorizationDecisionKey authZdecisionKeyEntry = com.janrain.backplane.server2.dao.BP2DAOs.authorizationDecisionKeyDao().get(authZdecisionKey).getOrElse(null); if (null == authZdecisionKeyEntry || ! authSessionCookie.equals(authZdecisionKeyEntry.get(AuthorizationDecisionKeyFields.AUTH_COOKIE()).getOrElse(null))) { throw new AuthorizationException(OAuth2.OAUTH2_AUTHZ_ACCESS_DENIED, "Presented authorization key was issued to a different authenticated bus owner.", authorizationRequest); } if (! "Authorize".equals(request.getParameter("authorize"))) { throw new AuthorizationException(OAuth2.OAUTH2_AUTHZ_ACCESS_DENIED, "Bus owner denied authorization.", authorizationRequest); } else { // todo: use (and check) scope posted back by bus owner Option<String> scopeOption = authorizationRequest.get(AuthorizationRequestFields.SCOPE()); String scopeString = checkScope(scopeOption.isDefined() ? scopeOption.get() : null, authenticatedBusOwner); // create grant/code Grant2 grant = new GrantBuilder(GrantType.AUTHORIZATION_CODE, GrantState.INACTIVE, authenticatedBusOwner, authorizationRequest.get(AuthorizationRequestFields.CLIENT_ID()).get(), scopeString).buildGrant(); BP2DAOs.grantDao().store(grant); logger.info("Authorized " + authorizationRequest.get(AuthorizationRequestFields.CLIENT_ID())+ "[" + authorizationRequest.get(AuthorizationRequestFields.COOKIE())+"]" + "grant ID: " + grant.id()); // return OAuth2 authz response final String code = grant.id(); Option<String> stateOption = authorizationRequest.get(AuthorizationRequestFields.STATE()); final String state = scopeOption.isDefined() ? scopeOption.get() : null; try { return new ModelAndView("redirect:" + UrlResponseFormat.QUERY.encode( authorizationRequest.getRedirectUri(), new HashMap<String, String>() {{ put(OAuth2.OAUTH2_AUTHZ_RESPONSE_CODE, code); if (StringUtils.isNotEmpty(state)) { put(OAuth2.OAUTH2_AUTHZ_RESPONSE_STATE, state); } }})); } catch (ValidationException ve) { String errMsg = "Error building (positive) authorization response: " + ve.getMessage(); logger.error(errMsg, ve); return authzRequestError(OAuth2.OAUTH2_AUTHZ_DIRECT_ERROR, errMsg, authorizationRequest.getRedirectUri(), (String)authorizationRequest.get(AuthorizationRequestFields.STATE()).getOrElse(null)); } } } catch (Exception e) { throw new AuthorizationException(OAuth2.OAUTH2_AUTHZ_SERVER_ERROR, e.getMessage(), authorizationRequest, e); } } private static ModelAndView authzRequestError( final String oauthErrCode, final String errMsg, final String redirectUri, final String state) { // direct or in/redirect if (OAuth2.OAUTH2_AUTHZ_DIRECT_ERROR.equals(oauthErrCode)) { logger.error("Authorization error: " + errMsg); return new ModelAndView(DIRECT_RESPONSE, new HashMap<String, Object>() {{ put(DIRECT_RESPONSE, errMsg); }}); } else { try { return new ModelAndView("redirect:" + UrlResponseFormat.QUERY.encode( redirectUri, new HashMap<String, String>() {{ put(OAuth2.OAUTH2_AUTHZ_ERROR_FIELD_NAME, oauthErrCode); put(OAuth2.OAUTH2_AUTHZ_ERROR_DESC_FIELD_NAME, errMsg); if (StringUtils.isNotEmpty(state)) { put(AuthorizationRequestFields.STATE().name(), state); } }})); } catch (ValidationException e) { logger.error("Error building redirect_uri: " + e.getMessage()); return new ModelAndView(DIRECT_RESPONSE, new HashMap<String, Object>() {{ put(DIRECT_RESPONSE, errMsg); }}); } } } /** * Throws AuthException if either of the following fail: * Client credentials MUST NOT be included in the request URI (OAuth2 2.3.1) * Client credentials in request body are NOT RECOMMENDED (OAuth2 2.3.1) * * @param queryString request query string * @param client_id from request parameters (may be from POST/request body) * @param client_secret from request parameters (may be from POST/request body) */ private void checkClientCredentialsBasicAuthOnly(String queryString, String client_id, String client_secret) throws AuthException { if (StringUtils.isNotEmpty(queryString)) { Map<String,String> queryParamsMap = new HashMap<String, String>(); for(String queryParamPair : Arrays.asList(queryString.split("&"))) { String[] nameVal = queryParamPair.split("=", 2); queryParamsMap.put(nameVal[0], nameVal.length >0 ? nameVal[1] : null); } if(queryParamsMap.containsKey("client_id") || queryParamsMap.containsKey("client_secret")) { authError("Client credentials MUST NOT be included in the request URI (OAuth2 2.3.1)"); } } if (StringUtils.isNotEmpty(client_id) || StringUtils.isNotEmpty(client_secret)) { authError("Client credentials in request body are NOT RECOMMENDED (OAuth2 2.3.1)"); } } private Backplane2Message parsePostedMessage(Map<String, Map<String, Object>> messagePostBody, Token token) throws BackplaneServerException, DaoException { List<Backplane2Message> result = new ArrayList<Backplane2Message>(); Map<String,Object> msg = messagePostBody.get("message"); if (msg == null) { // no message body? throw new InvalidRequestException("Missing message payload", HttpServletResponse.SC_BAD_REQUEST); } if (messagePostBody.keySet().size() != 1) { // other garbage in the payload throw new InvalidRequestException("Invalid data in payload", HttpServletResponse.SC_BAD_REQUEST); } String channelId = msg.get(Backplane2MessageFields.CHANNEL().name()) != null ? msg.get(Backplane2MessageFields.CHANNEL().name()).toString() : null; String bus = msg.get(Backplane2MessageFields.BUS().name()) != null ? msg.get(Backplane2MessageFields.BUS().name()).toString() : null; Channel channel = getChannel(channelId); String boundBus = channel == null ? null : (String) channel.get(ChannelFields.BUS()).getOrElse(null); if ( channel == null || ! StringUtils.equals(bus, boundBus)) { throw new InvalidRequestException("Invalid bus - channel binding ", HttpServletResponse.SC_FORBIDDEN); } // check to see if channel is already full if (BP2DAOs.messageDao().messageCount(channelId) >= BackplaneConfig.getDefaultMaxMessageLimit()) { throw new InvalidRequestException("Message limit of " + BackplaneConfig.getDefaultMaxMessageLimit() + " has been reached for channel '" + channel + "'", HttpServletResponse.SC_FORBIDDEN); } try { Backplane2Message message = new Backplane2Message( (String)token.get(TokenFields.CLIENT_SOURCE_URL()).getOrElse(null), Integer.parseInt( (String) channel.get(ChannelFields.MESSAGE_EXPIRE_DEFAULT_SECONDS()).getOrElse(null)), Integer.parseInt( (String) channel.get(ChannelFields.MESSAGE_EXPIRE_MAX_SECONDS()).getOrElse(null)), msg); if ( ! token.scope().isMessageInScope(message) ) { throw new InvalidRequestException("Invalid bus in message", HttpServletResponse.SC_FORBIDDEN); } return message; } catch (Exception e) { throw new InvalidRequestException("Invalid message data: " + e.getMessage(), HttpServletResponse.SC_BAD_REQUEST); } } private Channel getChannel(String channelId) throws BackplaneServerException, DaoException { Option<Channel> channel = BP2DAOs.channelDao().get(channelId); return channel.getOrElse(null); } private void aniLogNewChannel(HttpServletRequest request, String referer, String bus, String scope) { if (!anilogger.isEnabled()) { return; } int delim = scope.indexOf("channel:"); String channel = scope.substring(delim + 8); String channelId = "https://" + request.getServerName() + "/v2/bus/" + bus + "/channel/" + channel; String siteHost = (referer != null) ? ServletUtil.getHostFromUrl(referer) : null; Map<String,Object> aniEvent = new HashMap<String,Object>(); aniEvent.put("channel_id", channelId); aniEvent.put("bus", bus); aniEvent.put("site_host", siteHost); aniLog("new_channel", aniEvent); } private void aniLogPollMessages(HttpServletRequest request, String referer, List<Backplane2Message> messages) { if (!anilogger.isEnabled()) { return; } String siteHost = (referer != null) ? ServletUtil.getHostFromUrl(referer) : null; String serverName = request.getServerName(); Map<String,Object> aniEvent = new HashMap<String,Object>(); List<Map<String,String>> messagesMeta = new ArrayList<Map<String,String>>(); for (Backplane2Message message : messages) { String bus = message.bus(); String channelId = "https://" + serverName + "/v2/bus/" + bus + "/channel/" + message.channel(); Map<String,String> anotherMeta = new HashMap<String,String>(); anotherMeta.put("id", message.id()); anotherMeta.put("bus", bus); anotherMeta.put("channel_id", channelId); messagesMeta.add(anotherMeta); } aniEvent.put("messages", messagesMeta); aniEvent.put("site_host", siteHost); aniLog("poll_messages", aniEvent); } private void aniLogGetMessage(HttpServletRequest request, Backplane2Message message, Token token) { if (!anilogger.isEnabled()) { return; } String bus = message.bus(); String channelId = "https://" + request.getServerName() + "/v2/bus/" + bus + "/channel/" + message.channel(); Map<String,Object> aniEvent = new HashMap<String,Object>(); aniEvent.put("message_id", message.id()); aniEvent.put("bus", bus); aniEvent.put("channel_id", channelId); aniEvent.put("client_id", token.get(TokenFields.ISSUED_TO_CLIENT())); aniLog("get_message", aniEvent); } private void aniLogNewMessage(HttpServletRequest request, Backplane2Message message, Token token) { if (!anilogger.isEnabled()) { return; } String bus = message.bus(); String channel = message.channel(); String channelId = "https://" + request.getServerName() + "/v2/bus/" + bus + "/channel/" + channel; String clientId = (String) token.get(TokenFields.ISSUED_TO_CLIENT()).getOrElse(null); Map<String,Object> aniEvent = new HashMap<String,Object>(); aniEvent.put("channel_id", channelId); aniEvent.put("bus", bus); aniEvent.put("client_id", clientId); aniLog("new_message", aniEvent); } private void aniLog(String eventName, Map<String,Object> eventData) { ObjectMapper mapper = new ObjectMapper(); String time = DateTimeUtils.ISO8601.get().format(new Date(System.currentTimeMillis())); eventData.put("time", time); eventData.put("version", "v2"); try { anilogger.log(eventName, mapper.writeValueAsString(eventData)); } catch (Exception e) { String errMsg = "Error sending analytics event: " + e.getMessage(); logger.error(errMsg, BackplaneConfig.getDebugException(e)); } } private final com.yammer.metrics.core.Timer v2GetsTimer = com.yammer.metrics.Metrics.newTimer(new MetricName("v2", this.getClass().getName().replace(".","_"), "v2_gets_time"), TimeUnit.MILLISECONDS, TimeUnit.MINUTES); private final com.yammer.metrics.core.Timer v2GetSingleMessageTimer = com.yammer.metrics.Metrics.newTimer(new MetricName("v2", this.getClass().getName().replace(".","_"), "v2_get_single_message_time"), TimeUnit.MILLISECONDS, TimeUnit.MINUTES); private final com.yammer.metrics.core.Timer v2PostTimer = com.yammer.metrics.Metrics.newTimer(new MetricName("v2", this.getClass().getName().replace(".","_"), "v2_posts_time"), TimeUnit.MILLISECONDS, TimeUnit.SECONDS); private final com.yammer.metrics.core.Timer getRegularTokenTimer = com.yammer.metrics.Metrics.newTimer(new MetricName("v2", this.getClass().getName().replace(".","_"), "v2_get_reg_tokens_time"), TimeUnit.MILLISECONDS, TimeUnit.SECONDS); private final com.yammer.metrics.core.Timer getPrivilegedTokenTimer = com.yammer.metrics.Metrics.newTimer(new MetricName("v2", this.getClass().getName().replace(".","_"), "v2_get_privileged_tokens_time"), TimeUnit.MILLISECONDS, TimeUnit.SECONDS); }