package com.ibm.sbt.opensocial.domino.oauth; import java.io.PrintWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.shindig.auth.AnonymousSecurityToken; import org.apache.shindig.auth.SecurityToken; import org.apache.shindig.common.uri.Uri; import org.apache.shindig.gadgets.GadgetException; import org.apache.shindig.gadgets.http.HttpFetcher; import org.apache.shindig.gadgets.http.HttpRequest; import org.apache.shindig.gadgets.http.HttpResponse; import org.apache.shindig.gadgets.http.HttpResponseBuilder; import org.apache.shindig.gadgets.oauth2.OAuth2Accessor; import org.apache.shindig.gadgets.oauth2.OAuth2Arguments; import org.apache.shindig.gadgets.oauth2.OAuth2Error; import org.apache.shindig.gadgets.oauth2.OAuth2Message; import org.apache.shindig.gadgets.oauth2.OAuth2Request; import org.apache.shindig.gadgets.oauth2.OAuth2RequestException; import org.apache.shindig.gadgets.oauth2.OAuth2RequestParameterGenerator; import org.apache.shindig.gadgets.oauth2.OAuth2ResponseParams; import org.apache.shindig.gadgets.oauth2.OAuth2Token; import org.apache.shindig.gadgets.oauth2.OAuth2Utils; import org.apache.shindig.gadgets.oauth2.handler.AuthorizationEndpointResponseHandler; import org.apache.shindig.gadgets.oauth2.handler.ClientAuthenticationHandler; import org.apache.shindig.gadgets.oauth2.handler.GrantRequestHandler; import org.apache.shindig.gadgets.oauth2.handler.OAuth2HandlerError; import org.apache.shindig.gadgets.oauth2.handler.ResourceRequestHandler; import org.apache.shindig.gadgets.oauth2.handler.TokenEndpointResponseHandler; import com.google.common.collect.Maps; import com.google.inject.Inject; import com.google.inject.name.Named; public class DominoOAuth2Request implements OAuth2Request { private static final String CLASS = DominoOAuth2Request.class.getName(); private static final short MAX_ATTEMPTS = 3; private DominoOAuth2Accessor internalAccessor; private OAuth2Arguments arguments; private final List<AuthorizationEndpointResponseHandler> authorizationEndpointResponseHandlers; private final List<ClientAuthenticationHandler> clientAuthenticationHandlers; private final HttpFetcher fetcher; private final List<GrantRequestHandler> grantRequestHandlers; private HttpRequest realRequest; private final List<ResourceRequestHandler> resourceRequestHandlers; private OAuth2ResponseParams responseParams; private SecurityToken securityToken; private final DominoOAuth2TokenStore tokenStore; private final List<TokenEndpointResponseHandler> tokenEndpointResponseHandlers; private final boolean sendTraceToClient; private final OAuth2RequestParameterGenerator requestParameterGenerator; private short attemptCounter = 0; private final boolean viewerAccessTokensEnabled; private final Logger log; /** * @param tokenStore * configuration options for the fetcher * @param fetcher * fetcher to use for actually making requests */ @Inject public DominoOAuth2Request(final DominoOAuth2TokenStore tokenStore, final HttpFetcher fetcher, final List<AuthorizationEndpointResponseHandler> authorizationEndpointResponseHandlers, final List<ClientAuthenticationHandler> clientAuthenticationHandlers, final List<GrantRequestHandler> grantRequestHandlers, final List<ResourceRequestHandler> resourceRequestHandlers, final List<TokenEndpointResponseHandler> tokenEndpointResponseHandlers, final boolean sendTraceToClient, final OAuth2RequestParameterGenerator requestParameterGenerator, @Named("shindig.oauth2.viewer-access-tokens-enabled") final boolean viewerAccessTokensEnabled, Logger log) { this.viewerAccessTokensEnabled = viewerAccessTokensEnabled; this.log = log; this.tokenStore = tokenStore; this.fetcher = fetcher; this.authorizationEndpointResponseHandlers = authorizationEndpointResponseHandlers; this.clientAuthenticationHandlers = clientAuthenticationHandlers; this.grantRequestHandlers = grantRequestHandlers; this.resourceRequestHandlers = resourceRequestHandlers; this.tokenEndpointResponseHandlers = tokenEndpointResponseHandlers; this.sendTraceToClient = sendTraceToClient; this.requestParameterGenerator = requestParameterGenerator; } public HttpResponse fetch(final HttpRequest request) { final String method = "fetch"; log.entering(DominoOAuth2Request.CLASS, method, request); DominoOAuth2Accessor accessor = null; HttpResponse response = null; this.responseParams = new OAuth2ResponseParams(); try { // First step is to get an OAuth2Accessor for this request if (request == null || request.getSecurityToken() == null) { // Any errors before we have an accessor are special cases response = this.sendErrorResponse(null, OAuth2Error.MISSING_FETCH_PARAMS, "no request or security token"); } else { this.realRequest = request; this.securityToken = request.getSecurityToken(); if(securityToken.isAnonymous() || "@anonymous".equals(securityToken.getViewerId())) { return this.sendErrorResponse(null, OAuth2Error.GET_OAUTH2_ACCESSOR_PROBLEM, "Anonymous users cannot use OAuth 2 within gadgets."); } this.arguments = this.realRequest.getOAuth2Arguments(); if (this.arguments == null) { // Any errors before we have an accessor are special cases return this.sendErrorResponse(null, OAuth2Error.FETCH_INIT_PROBLEM, "no responseParams or arguments"); } accessor = this.getAccessor(); if (accessor == null) { // Any errors before we have an accessor are special cases response = this.sendErrorResponse(null, OAuth2Error.FETCH_INIT_PROBLEM, "accessor is null"); } else { accessor.setRedirecting(false); final Map<String, String> requestParams = this.requestParameterGenerator .generateParams(this.realRequest); accessor.setAdditionalRequestParams(requestParams); HttpResponseBuilder responseBuilder = null; if (!accessor.isErrorResponse()) { responseBuilder = this.attemptFetch(accessor); } response = this.processResponse(accessor, responseBuilder); } } } catch (final Throwable t) { log.logp(Level.WARNING, CLASS, method, "exception occurred during fetch", t); if (accessor == null) { accessor = new BasicDominoOAuth2Accessor(); accessor.setErrorResponse(t, OAuth2Error.FETCH_PROBLEM, "exception occurred during fetch", ""); } else { accessor.setErrorResponse(t, OAuth2Error.FETCH_PROBLEM, "exception occurred during fetch", ""); } response = this.processResponse(accessor, this.getErrorResponseBuilder(t, OAuth2Error.FETCH_PROBLEM, "exception occurred during fetch")); } finally { if (accessor != null) { if (!accessor.isRedirecting()) { if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "accessor is not redirecting, remove it", accessor); } accessor.invalidate(); try { this.tokenStore.removeOAuth2Accessor(accessor); } catch (GadgetException e) { log.logp(Level.WARNING, CLASS, method, "Error removing OAuth2Accessor from store.", e); } this.internalAccessor = null; } else { if (!accessor.isValid()) { if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "accesssor is not valid", accessor); } } else if (accessor.isErrorResponse()) { if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "accessor isErrorResponse", accessor.getErrorContextMessage()); } } try { this.tokenStore.storeOAuth2Accessor(accessor); } catch (GadgetException e) { log.logp(Level.WARNING, CLASS, method, "Error removing OAuth2 accessor from store."); } } } } log.exiting(CLASS, method, response); return response; } private HttpResponseBuilder attemptFetch(final DominoOAuth2Accessor accessor) { final String method = "attemptFetch"; log.entering(CLASS, method, accessor); if (this.attemptCounter > DominoOAuth2Request.MAX_ATTEMPTS) { if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "MAX_ATTEMPTS exceeded {0}", this.attemptCounter); // This can be useful to diagnose the recursion final StackTraceElement[] stackElements = Thread.currentThread().getStackTrace(); String stack = ""; for (final StackTraceElement element : stackElements) { stack = stack + element.toString() + "\n"; } log.logp(Level.FINEST, CLASS, method, "MAX_ATTEMPTS stack = {0}", stack); } return this.fetchData(accessor, true); } this.attemptCounter++; if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "attempt number {0}", this.attemptCounter); } HttpResponseBuilder ret = null; if (accessor.isErrorResponse()) { // If there's an error in the accessor don't continue. return this.getErrorResponseBuilder(accessor.getErrorException(), accessor.getError(), accessor.getErrorContextMessage(), accessor.getErrorUri(), accessor.getErrorContextMessage()); } if(!DominoOAuth2Request.haveAccessToken(accessor) && !DominoOAuth2Request.haveRefreshToken(accessor)) { if (!accessor.isRedirecting() && this.checkCanAuthorize(accessor)) { final String completeAuthUrl = this.authorize(accessor); if (completeAuthUrl != null) { // Send a response to redirect to the authorization url this.responseParams.setAuthorizationUrl(completeAuthUrl); accessor.setRedirecting(true); } else { // This wasn't a redirect type of authorization. try again ret = this.attemptFetch(accessor); } } } if (DominoOAuth2Request.haveAccessToken(accessor)) { // We have an access_token, use it and stop! // Don't try more than three times ret = this.fetchData(accessor, this.attemptCounter > DominoOAuth2Request.MAX_ATTEMPTS); } else if (DominoOAuth2Request.haveRefreshToken(accessor)) { // We don't have an access token, we need to try and get one. // First step see if we have a refresh token ret = refreshAccessToken(accessor); } if (ret == null) { if (accessor.isRedirecting()) { // Send redirect response to client ret = new HttpResponseBuilder().setHttpStatusCode(HttpResponse.SC_OK).setStrictNoCache(); } else { accessor.setAccessToken(null); ret = this.attemptFetch(accessor); } } log.exiting(CLASS, method, ret); return ret; } protected HttpResponseBuilder refreshAccessToken(DominoOAuth2Accessor accessor) { final String method = "refreshAccessToken"; HttpResponseBuilder ret = null; boolean attempt = false; final String internedAccessor = getAccessorKey(accessor).intern(); // This syncrhonized block is less than ideal. // It is needed because if a gadget has multiple makeRequests that triggers // multiple refreshes they can end up clobbering each other, and cause // temporary failures until the gadget is refreshed. // Syncrhonizing on the internedAccessor helps. It is not cluster safe // and could be problematic having so much code synchd. // TODO : https://issues.apache.org/jira/browse/SHINDIG-1871 synchronized (internedAccessor) { final OAuth2Accessor acc = this.getAccessorInternal(); if (DominoOAuth2Request.haveAccessToken(acc)) { // Another refresh must have won if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "found an access token from another refresh", new Object[] {}); } attempt = true; } else { final OAuth2HandlerError handlerError = this.refreshToken(accessor); if (handlerError == null) { // No errors refreshing, attempt the fetch again. attempt = true; if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "no refresh errors reported"); } } else { if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "refresh errors reported"); } // There was an error refreshing, stop. final OAuth2Error error = handlerError.getError(); ret = this.getErrorResponseBuilder(handlerError.getCause(), error, handlerError.getContextMessage(), handlerError.getUri(), handlerError.getDescription()); } } } if (attempt) { if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "going to re-attempt with a clean accesor"); } try { this.tokenStore.removeOAuth2Accessor(this.internalAccessor); } catch (GadgetException e) { log.logp(Level.WARNING, CLASS, method, "Error while removing accessor.", e); } this.internalAccessor = null; ret = this.attemptFetch(this.getAccessor()); } return ret; } private String authorize(final OAuth2Accessor accessor) { final String method = "authorize"; log.entering(CLASS, method, accessor); String ret = null; final String grantType = accessor.getGrantType(); GrantRequestHandler grantRequestHandlerUsed = null; for (final GrantRequestHandler grantRequestHandler : this.grantRequestHandlers) { if (grantRequestHandler.getGrantType().equalsIgnoreCase(grantType)) { grantRequestHandlerUsed = grantRequestHandler; break; } } if (grantRequestHandlerUsed == null) { accessor.setErrorResponse(null, OAuth2Error.AUTHENTICATION_PROBLEM, "no grantRequestHandler found for " + grantType, ""); } else { String completeAuthUrl = null; try { completeAuthUrl = grantRequestHandlerUsed.getCompleteUrl(accessor); } catch (final OAuth2RequestException e) { log.logp(Level.WARNING, CLASS, method, "error getting complete url", e); } if (grantRequestHandlerUsed.isRedirectRequired()) { ret = completeAuthUrl; } else { final OAuth2HandlerError error = this.authorize(accessor, grantRequestHandlerUsed, completeAuthUrl); if (error != null) { accessor.setErrorResponse(error.getCause(), OAuth2Error.AUTHENTICATION_PROBLEM, error.getContextMessage() + " , " + error.getDescription(), error.getUri()); } } } log.exiting(CLASS, method, ret); return ret; } private OAuth2HandlerError authorize(final OAuth2Accessor accessor, final GrantRequestHandler grantRequestHandler, final String completeAuthUrl) { final String method = "authorize"; log.entering(CLASS, method, new Object[] { accessor, grantRequestHandler, completeAuthUrl }); OAuth2HandlerError ret = null; HttpRequest authorizationRequest; try { authorizationRequest = grantRequestHandler.getAuthorizationRequest(accessor, completeAuthUrl); } catch (final OAuth2RequestException e) { authorizationRequest = null; ret = new OAuth2HandlerError(e.getError(), e.getErrorText(), e); } if (authorizationRequest != null) { HttpResponse authorizationResponse; try { authorizationResponse = this.fetcher.fetch(authorizationRequest); } catch (final GadgetException e) { log.logp(Level.WARNING, CLASS, method, "Exception while making authorizating request", e); authorizationResponse = null; ret = new OAuth2HandlerError(OAuth2Error.AUTHORIZE_PROBLEM, "exception thrown fetching authorization", e); } if (authorizationResponse != null) { if (grantRequestHandler.isAuthorizationEndpointResponse()) { for (final AuthorizationEndpointResponseHandler authorizationEndpointResponseHandler : this.authorizationEndpointResponseHandlers) { if (authorizationEndpointResponseHandler.handlesResponse(accessor, authorizationResponse)) { ret = authorizationEndpointResponseHandler.handleResponse(accessor, authorizationResponse); if (ret != null) { // error occurred stop processing break; } } } } if (ret == null) { if (grantRequestHandler.isTokenEndpointResponse()) { for (final TokenEndpointResponseHandler tokenEndpointResponseHandler : this.tokenEndpointResponseHandlers) { if (tokenEndpointResponseHandler.handlesResponse(accessor, authorizationResponse)) { ret = tokenEndpointResponseHandler.handleResponse(accessor, authorizationResponse); if (ret != null) { // error occurred stop processing break; } } } } } } } log.exiting(CLASS, method, ret); return ret; } private String buildRefreshTokenUrl(final OAuth2Accessor accessor) { final String method = "buildRefreshTokenUrl"; log.entering(CLASS, method, accessor); String ret = null; final String refreshUrl = accessor.getTokenUrl(); if (refreshUrl != null) { ret = DominoOAuth2Request.getCompleteRefreshUrl(refreshUrl); } log.exiting(CLASS, method, ret); return ret; } private boolean checkCanAuthorize(final OAuth2Accessor accessor) { final String method = "checkCanAuthorize"; log.entering(CLASS, method, accessor); boolean ret = true; final String pageOwner = this.securityToken.getOwnerId(); final String pageViewer = this.securityToken.getViewerId(); if (pageOwner == null || pageViewer == null) { accessor.setErrorResponse(null, OAuth2Error.AUTHORIZE_PROBLEM, "pageOwner or pageViewer is null", ""); ret = false; } else if (!this.viewerAccessTokensEnabled && !pageOwner.equals(pageViewer)) { accessor.setErrorResponse(null, OAuth2Error.AUTHORIZE_PROBLEM, "pageViewer is not pageOwner", ""); ret = false; } log.exiting(CLASS, method, ret); return ret; } private HttpResponseBuilder fetchData(final DominoOAuth2Accessor accessor, final boolean lastAttempt) { final String method = "fetchData"; log.entering(CLASS, method, accessor); HttpResponseBuilder ret = null; try { final HttpResponse response = this.fetchFromServer(accessor, this.realRequest, lastAttempt); if (response != null) { ret = new HttpResponseBuilder(response); if (response.getHttpStatusCode() != HttpResponse.SC_OK && this.sendTraceToClient) { this.responseParams.addRequestTrace(this.realRequest, response); } } } catch (final OAuth2RequestException e) { ret = this.getErrorResponseBuilder(e, e.getError(), e.getErrorText(), e.getErrorUri(), e.getErrorDescription()); } log.exiting(CLASS, method, ret); return ret; } private HttpResponse fetchFromServer(final DominoOAuth2Accessor accessor, final HttpRequest request, final boolean lastAttempt) throws OAuth2RequestException { final String method = "fetchFromServer"; log.entering(CLASS, method, new Object[] { accessor, lastAttempt }); HttpResponse ret; final long currentTime = System.currentTimeMillis(); OAuth2Token accessToken = accessor.getAccessToken(); if (accessToken != null) { final long expiresAt = accessToken.getExpiresAt(); if (expiresAt != 0) { if (currentTime >= expiresAt) { if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "accessToken has expired at {0}", new Object[]{expiresAt}); } try { this.tokenStore.removeAccessToken(accessor); } catch (final GadgetException e) { throw new OAuth2RequestException(OAuth2Error.MISSING_SERVER_RESPONSE, "error removing access_token", null); } accessToken = null; accessor.setAccessToken(null); if (!lastAttempt) { return null; } } } } OAuth2Token refreshToken = accessor.getRefreshToken(); if (refreshToken != null) { final long expiresAt = refreshToken.getExpiresAt(); if (expiresAt != 0) { if (currentTime >= expiresAt) { if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "refreshToken has expired at {0}", new Object[]{expiresAt}); } try { this.tokenStore.removeRefreshToken(accessor); } catch (final GadgetException e) { throw new OAuth2RequestException(OAuth2Error.MISSING_SERVER_RESPONSE, "error removing refresh_token", null); } refreshToken = null; accessor.setRefreshToken(null); if (!lastAttempt) { return null; } } } } if (accessToken != null) { final boolean isAllowed = isUriAllowed(request.getUri(), accessor.getAllowedDomains()); if (isAllowed) { String tokenType = accessToken.getTokenType(); if (tokenType == null || tokenType.length() == 0) { tokenType = OAuth2Message.BEARER_TOKEN_TYPE; } for (final ResourceRequestHandler resourceRequestHandler : this.resourceRequestHandlers) { if (tokenType.equalsIgnoreCase(resourceRequestHandler.getTokenType())) { resourceRequestHandler.addOAuth2Params(accessor, request); } } } else { log.logp(Level.WARNING, CLASS, method, "Gadget {0} attempted to send OAuth2 Token to an unauthorized domain: {1}.", new Object[] { accessor.getGadgetUri(), request.getUri() }); throw new OAuth2RequestException(OAuth2Error.SERVER_REJECTED_REQUEST, "The accessor is not allowed to be sent to the domain of the request.", null); } } try { ret = this.fetcher.fetch(request); } catch (final GadgetException e) { throw new OAuth2RequestException(OAuth2Error.MISSING_SERVER_RESPONSE, "GadgetException fetchFromServer", e); } final int responseCode = ret.getHttpStatusCode(); if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "responseCode = {0}", new Object[]{responseCode}); } if (responseCode == HttpResponse.SC_UNAUTHORIZED) { if (accessToken != null) { try { this.tokenStore.removeAccessToken(accessor); } catch (final GadgetException e) { throw new OAuth2RequestException(OAuth2Error.MISSING_SERVER_RESPONSE, "error removing access_token", null); } accessor.setAccessToken(null); } if (!lastAttempt) { ret = null; } } log.exiting(CLASS, method, ret); return ret; } private DominoOAuth2Accessor getAccessorInternal() { DominoOAuth2Accessor ret = tokenStore.getOAuth2Accessor(this.securityToken, this.arguments, this.realRequest.getGadget()); return ret; } private DominoOAuth2Accessor getAccessor() { if (this.internalAccessor == null || !this.internalAccessor.isValid()) { this.internalAccessor = this.getAccessorInternal(); } return this.internalAccessor; } private static String getCompleteRefreshUrl(final String refreshUrl) { return OAuth2Utils.buildUrl(refreshUrl, null, null); } private HttpResponseBuilder getErrorResponseBuilder(final Throwable t, final OAuth2Error error, final String contextMessage) { return getErrorResponseBuilder(t, error, contextMessage, null, null); } private HttpResponseBuilder getErrorResponseBuilder(final Throwable t, final OAuth2Error error, final String contextMessage, final String errorUri, final String errorDescription) { final String method = "getErrorResponseBuilder"; log.entering(CLASS, method, new Object[] { t, error, contextMessage, errorUri, errorDescription }); final HttpResponseBuilder ret = new HttpResponseBuilder().setHttpStatusCode( HttpResponse.SC_FORBIDDEN).setStrictNoCache(); if (t != null && this.sendTraceToClient) { final StringWriter sw = new StringWriter(); t.printStackTrace(new PrintWriter(sw)); final String message = sw.toString(); this.responseParams.addDebug(message); } if (this.sendTraceToClient) { this.responseParams.addToResponse(ret, error.getErrorCode(), error.getErrorDescription(contextMessage) + " , " + errorDescription, errorUri, error.getErrorExplanation()); } else { this.responseParams.addToResponse(ret, error.getErrorCode(), "", "", error.getErrorExplanation()); } log.exiting(CLASS, method, ret); return ret; } private String getRefreshBody(final OAuth2Accessor accessor) { final String method = "getRefreshBody"; log.entering(CLASS, method, accessor); String ret = ""; Map<String, String> queryParams; try { queryParams = Maps.newHashMap(); queryParams.put(OAuth2Message.GRANT_TYPE, OAuth2Message.REFRESH_TOKEN); queryParams.put(OAuth2Message.REFRESH_TOKEN, new String(accessor.getRefreshToken() .getSecret(), "UTF-8")); if (accessor.getScope() != null && accessor.getScope().length() > 0) { queryParams.put(OAuth2Message.SCOPE, accessor.getScope()); } final String clientId = accessor.getClientId(); final byte[] secret = accessor.getClientSecret(); queryParams.put(OAuth2Message.CLIENT_ID, clientId); queryParams.put(OAuth2Message.CLIENT_SECRET, new String(secret, "UTF-8")); ret = OAuth2Utils.buildUrl(ret, queryParams, null); final char firstChar = ret.charAt(0); if (firstChar == '?' || firstChar == '&') { ret = ret.substring(1); } } catch (final UnsupportedEncodingException e) { log.logp(Level.WARNING, CLASS, method, "error generating refresh body", e); ret = null; } log.exiting(CLASS, method, ret); return ret; } private HttpResponse processResponse(final OAuth2Accessor accessor, final HttpResponseBuilder responseBuilder) { final String method = "processResponse"; log.entering(CLASS, method,new Object[] { accessor, responseBuilder }); if (accessor.isErrorResponse() || responseBuilder == null) { return this.sendErrorResponse(accessor.getErrorException(), accessor.getError(), accessor.getErrorContextMessage(), accessor.getErrorUri(), ""); } if (this.responseParams.getAuthorizationUrl() != null) { responseBuilder.setMetadata(OAuth2ResponseParams.APPROVAL_URL, this.responseParams.getAuthorizationUrl()); accessor.setRedirecting(true); } else { accessor.setRedirecting(false); } final HttpResponse ret = responseBuilder.create(); log.exiting(CLASS, method); return ret; } private OAuth2HandlerError refreshToken(final DominoOAuth2Accessor accessor) { final String method = "refreshToken"; log.entering(CLASS, method, new Object[] { accessor }); OAuth2HandlerError ret = null; String refershTokenUrl; refershTokenUrl = buildRefreshTokenUrl(accessor); if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "refershTokenUrl = {0}", new Object[]{refershTokenUrl}); } if (refershTokenUrl != null) { HttpResponse response = null; final HttpRequest request = new HttpRequest(Uri.parse(refershTokenUrl)); request.setSecurityToken(new AnonymousSecurityToken("", 0L, accessor.getGadgetUri())); request.setMethod("POST"); request.setHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); for (final ClientAuthenticationHandler clientAuthenticationHandler : this.clientAuthenticationHandlers) { if (clientAuthenticationHandler.geClientAuthenticationType().equalsIgnoreCase( accessor.getClientAuthenticationType())) { clientAuthenticationHandler.addOAuth2Authentication(request, accessor); } } try { final byte[] body = getRefreshBody(accessor).getBytes("UTF-8"); request.setPostBody(body); } catch (final Exception e) { log.logp(Level.WARNING, CLASS, method, "Error while getting body for refresh request.", e); ret = new OAuth2HandlerError(OAuth2Error.REFRESH_TOKEN_PROBLEM, "error generating refresh body", e); } if (!isUriAllowed(request.getUri(), accessor.getAllowedDomains())) { ret = new OAuth2HandlerError(OAuth2Error.REFRESH_TOKEN_PROBLEM, "error fetching refresh token - domain not allowed", null); } if (ret == null) { try { response = this.fetcher.fetch(request); } catch (final GadgetException e) { log.logp(Level.WARNING, CLASS, method, "Error while making refresh request.", e); ret = new OAuth2HandlerError(OAuth2Error.REFRESH_TOKEN_PROBLEM, "error fetching refresh token", e); } if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "response = {0}", new Object[] {response}); } if (ret == null) { // response is not null.. final int statusCode = response.getHttpStatusCode(); if (statusCode == HttpResponse.SC_UNAUTHORIZED || statusCode == HttpResponse.SC_BAD_REQUEST) { try { this.tokenStore.removeRefreshToken(accessor); } catch (final GadgetException e) { ret = new OAuth2HandlerError(OAuth2Error.REFRESH_TOKEN_PROBLEM, "failed to remove refresh token", e); } accessor.setRefreshToken(null); if (log.isLoggable(Level.FINEST)) { log.logp(Level.FINEST, CLASS, method, "received {0} from provider, removed refresh token. response = {1}", new Object[] { statusCode, response.getResponseAsString() }); } return ret; } else if (statusCode != HttpResponse.SC_OK) { ret = new OAuth2HandlerError(OAuth2Error.REFRESH_TOKEN_PROBLEM, "bad response from server : " + statusCode, null, "", response.getResponseAsString()); } if (ret == null) { for (final TokenEndpointResponseHandler tokenEndpointResponseHandler : this.tokenEndpointResponseHandlers) { if (tokenEndpointResponseHandler.handlesResponse(accessor, response)) { final OAuth2HandlerError error = tokenEndpointResponseHandler.handleResponse( accessor, response); if (error != null) { try { this.tokenStore.removeRefreshToken(accessor); } catch (GadgetException e) { log.logp(Level.WARNING, CLASS, method, "There was an error removing the refresh token after an error occured processing the token refresh response.", e); } accessor.setRefreshToken(null); return error; } } } } } } } log.exiting(CLASS, method, ret); return ret; } private HttpResponse sendErrorResponse(final Throwable t, final OAuth2Error error, final String contextMessage) { final HttpResponseBuilder responseBuilder = this.getErrorResponseBuilder(t, error, contextMessage); return responseBuilder.create(); } private HttpResponse sendErrorResponse(final Throwable t, final OAuth2Error error, final String contextMessage, final String errorUri, final String errorDescription) { final HttpResponseBuilder responseBuilder = this.getErrorResponseBuilder(t, error, contextMessage, errorUri, errorDescription); return responseBuilder.create(); } private static boolean haveAccessToken(final OAuth2Accessor accessor) { OAuth2Token token = accessor.getAccessToken(); return token != null && DominoOAuth2Request.validateAccessToken(token); } private static boolean haveRefreshToken(final OAuth2Accessor accessor) { OAuth2Token token = accessor.getRefreshToken(); return token != null && DominoOAuth2Request.validateRefreshToken(token); } private static boolean isUriAllowed(final Uri uri, final String[] allowedDomains) { if (allowedDomains == null || allowedDomains.length == 0) { // if white list is not specified, allow client to access any domain return true; } String host = uri.getAuthority(); final int pos = host.indexOf(':'); if (pos != -1) { host = host.substring(0, pos); } for (String domain : allowedDomains) { if (domain != null) { domain = domain.trim(); if (domain.startsWith(".") && host.endsWith(domain) || domain.equals(host)) { return true; } } } return false; } private static boolean validateAccessToken(final OAuth2Token accessToken) { return accessToken != null; } private static boolean validateRefreshToken(final OAuth2Token refreshToken) { return refreshToken != null; } private static String getAccessorKey(final OAuth2Accessor accessor) { if (accessor != null) { return "accessor:" + accessor.getGadgetUri() + ':' + accessor.getServiceName() + ':' + accessor.getUser() + ':' + accessor.getScope(); } return null; } }