package com.ibm.sbt.opensocial.domino.servlets;
import java.io.IOException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shindig.common.crypto.BlobCrypter;
import org.apache.shindig.common.servlet.HttpUtil;
import org.apache.shindig.common.servlet.InjectedServlet;
import org.apache.shindig.gadgets.GadgetException;
import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
import org.apache.shindig.gadgets.oauth2.OAuth2Error;
import org.apache.shindig.gadgets.oauth2.OAuth2FetcherConfig;
import org.apache.shindig.gadgets.oauth2.OAuth2Message;
import org.apache.shindig.gadgets.oauth2.OAuth2Module;
import org.apache.shindig.gadgets.oauth2.handler.AuthorizationEndpointResponseHandler;
import org.apache.shindig.gadgets.oauth2.handler.OAuth2HandlerError;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.ibm.sbt.opensocial.domino.oauth.DominoOAuth2Accessor;
import com.ibm.sbt.opensocial.domino.oauth.DominoOAuth2CallbackState;
import com.ibm.sbt.opensocial.domino.oauth.DominoOAuth2TokenStore;
/**
* Callback servlet for 3-legged OAuth 2.0 dance.
*
*/
public class DominoOAuth2CallbackServlet extends InjectedServlet {
private static final long serialVersionUID = -190882288947178518L;
private static final String CLASS = DominoOAuth2CallbackServlet.class.getName();
private transient List<AuthorizationEndpointResponseHandler> authorizationEndpointResponseHandlers;
private transient DominoOAuth2TokenStore store;
private transient Provider<OAuth2Message> oauth2MessageProvider;
private transient BlobCrypter stateCrypter;
private transient boolean sendTraceToClient = false;
private Logger log;
// This bit of magic passes the entire callback URL into the opening gadget
// for later use.
// gadgets.io.makeRequest (or osapi.oauth) will then pick up the callback URL
// to complete the
// oauth dance.
private static final String RESP_BODY = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" "
+ "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>Close this window</title>\n"
+ "</head>\n"
+ "<body>\n"
+ "<script type='text/javascript'>\n"
+ "try {\n"
+ " window.opener.gadgets.io.oauthReceivedCallbackUrl_ = document.location.href;\n"
+ "} catch (e) {\n"
+ "}\n"
+ "window.close();\n"
+ "</script>\n"
+ "Close this window.\n" + "</body>\n" + "</html>\n";
private static final String RESP_ERROR_BODY = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" "
+ "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>OAuth2 Error</title>\n"
+ "</head>\n"
+ "<body>\n"
+ "<p>error = %s</p>"
+ "<p>error description = %s</p>"
+ "<p>error uri = %s</p>"
+ "Close this window.\n"
+ "</body>\n" + "</html>\n";
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse resp)
throws IOException {
final String method = "doGet";
DominoOAuth2Accessor accessor = null;
final OAuth2Message msg = this.oauth2MessageProvider.get();
msg.parseRequest(request);
if(!isOAuthMsgValid(msg, resp)) {
return;
}
final DominoOAuth2CallbackState state = new DominoOAuth2CallbackState(this.stateCrypter,
msg.getState());
try {
accessor = this.store.getOAuth2Accessor(state);
} catch (GadgetException e1) {
log.logp(Level.WARNING, CLASS, method, "Error getting accessor from store.", e1);
}
if(accessor == null) {
sendError(OAuth2Error.CALLBACK_PROBLEM, "OAuth2CallbackServlet accessor is null",
"OAuth2CallbackServlet accessor is null", "", null, resp,
null, this.sendTraceToClient);
return;
}
if(!isAccessorValid(accessor, resp)) {
accessor.invalidate();
try {
this.store.removeOAuth2Accessor(accessor);
} catch (GadgetException e) {
log.logp(Level.WARNING, CLASS, method, "Error removing invalid accessor.", e);
}
return;
}
try {
boolean foundHandler = false;
for (final AuthorizationEndpointResponseHandler authorizationEndpointResponseHandler : this.authorizationEndpointResponseHandlers) {
if (authorizationEndpointResponseHandler.handlesRequest(accessor, request)) {
final OAuth2HandlerError handlerError = authorizationEndpointResponseHandler
.handleRequest(accessor, request);
if (handlerError != null) {
sendError(handlerError.getError(),
handlerError.getContextMessage(), handlerError.getDescription(),
handlerError.getUri(), accessor, resp, handlerError.getCause(),
this.sendTraceToClient);
return;
}
foundHandler = true;
break;
}
}
if (!foundHandler) {
sendError(OAuth2Error.NO_RESPONSE_HANDLER,
"OAuth2Callback servlet couldn't find a AuthorizationEndpointResponseHandler", "",
"", accessor, resp, null, this.sendTraceToClient);
return;
}
HttpUtil.setNoCache(resp);
resp.setContentType("text/html; charset=UTF-8");
resp.getWriter().write(RESP_BODY);
} catch (final Exception e) {
sendError(OAuth2Error.CALLBACK_PROBLEM,
"Exception occurred processing redirect.", "", "", accessor, resp, e,
this.sendTraceToClient);
throw new IOException(e);
} finally {
try{
if (!accessor.isErrorResponse()) {
accessor.invalidate();
this.store.removeOAuth2Accessor(accessor);
} else {
this.store.storeOAuth2Accessor(accessor);
}
} catch(GadgetException e) {
log.logp(Level.WARNING, CLASS, method, "Error storing/removing accessor.", e);
throw new IOException(e);
}
}
}
/**
* Validates the OAuth message.
* @param msg The OAuth message.
* @param resp The response. This method should write an error response.
* @return True if the OAuth message is valid false otherwise.
* @throws IOException Thrown when there is an error writing the error response.
*/
protected boolean isOAuthMsgValid(OAuth2Message msg, HttpServletResponse resp) throws IOException {
boolean result = true;
final OAuth2Error error = msg.getError();
if (error != null) {
sendError(error, "encRequestStateKey is null", msg.getErrorDescription(),
msg.getErrorUri(), null, resp, null, this.sendTraceToClient);
result = false;
}
final String encRequestStateKey = msg.getState();
if (encRequestStateKey == null) {
sendError(OAuth2Error.CALLBACK_PROBLEM,
"OAuth2CallbackServlet requestStateKey is null.", "", "", null, resp, null,
this.sendTraceToClient);
result = false;
}
return result;
}
/**
* Validates the OAuth accessor.
* @param accessor The OAuth 2.0 accessor object to validate.
* @param resp The response. This method should write an error response.
* @return True if the OAuth accessor is valid, false otherwise.
* @throws IOException Thrown when there is an error writing the error response.
*/
protected boolean isAccessorValid(DominoOAuth2Accessor accessor, HttpServletResponse resp) throws IOException {
if(!accessor.isValid()) {
sendError(OAuth2Error.CALLBACK_PROBLEM, "OAuth2CallbackServlet accessor is invalid " + accessor,
accessor.getErrorContextMessage(), accessor.getErrorUri(), accessor, resp,
accessor.getErrorException(), this.sendTraceToClient);
return false;
}
if(accessor.isErrorResponse()) {
sendError(OAuth2Error.CALLBACK_PROBLEM, "OAuth2CallbackServlet accessor isErrorResponse " + accessor,
accessor.getErrorContextMessage(), accessor.getErrorUri(), accessor, resp,
accessor.getErrorException(), this.sendTraceToClient);
return false;
}
if (!accessor.isRedirecting()) {
// Somehow our accessor got lost. We should not proceed.
sendError(OAuth2Error.CALLBACK_PROBLEM,
"OAuth2CallbackServlet accessor is not valid, isn't redirecting.", "", "",
accessor, resp, null, this.sendTraceToClient);
return false;
}
return true;
}
/**
* Sends an error back to the client.
* @param error The error that occurred.
* @param contextMessage The message to send to the client.
* @param description A description of the the error.
* @param uri The error URI.
* @param accessor The OAuth 2.0 accessor.
* @param resp The response object.
* @param t A throwable.
* @param sendTraceToClient True to send the stack trace to the client false otherwise.
* @throws IOException Thrown if there is an error writing the response.
*/
protected void sendError(final OAuth2Error error, final String contextMessage,
final String description, final String uri, final OAuth2Accessor accessor,
final HttpServletResponse resp, final Throwable t, final boolean sendTraceToClient)
throws IOException {
final String method = "sendError";
log.logp(Level.WARNING, CLASS, method, CLASS + " , callback error "
+ error + " - " + contextMessage + " , " + description + " - " + uri);
if (t != null) {
if (log.isLoggable(Level.FINEST)) {
log.logp(Level.FINE, CLASS, method, "callback exception", t);
}
}
HttpUtil.setNoCache(resp);
resp.setContentType("text/html; charset=UTF-8");
if (accessor != null) {
accessor.setErrorResponse(t, error, contextMessage + " , " + description, uri);
} else {
// We don't have an accessor to report the error back to the client in the
// normal manner.
// Anything is better than nothing, hack something together....
final String errorResponse;
if (sendTraceToClient) {
errorResponse = String.format(RESP_ERROR_BODY, error.getErrorCode(),
error.getErrorDescription(description), uri);
} else {
errorResponse = String.format(RESP_ERROR_BODY, error.getErrorCode(),
"", "");
}
resp.getWriter().write(errorResponse);
return;
}
resp.getWriter().write(RESP_BODY);
}
/**
* Sets the authorization response handlers.
* @param authorizationEndpointResponseHandlers The authorization response handlers.
*/
@Inject
public void setAuthorizationResponseHandlers(
final List<AuthorizationEndpointResponseHandler> authorizationEndpointResponseHandlers) {
this.authorizationEndpointResponseHandlers = authorizationEndpointResponseHandlers;
}
/**
* Sets the logger.
* @param log The logger.
*/
@Inject
public void setLogger(Logger log) {
this.log = log;
}
/**
* Indicates if trace information should be sent to the client in the case of an error.
* @param sendTraceToClient True to send trace information to the client, false otherwise.
*/
@Inject
public void sendTraceToClient(@Named(OAuth2Module.SEND_TRACE_TO_CLIENT)
final boolean sendTraceToClient) {
this.sendTraceToClient = sendTraceToClient;
}
/**
* Sets the OAuth 2.0 store.
* @param store The OAuth 2.0 store.
*/
@Inject
public void setOAuth2Store(final DominoOAuth2TokenStore store) {
this.store = store;
}
/**
* Sets the OAuth 2.0 message provider.
* @param oauth2MessageProvider The OAuth 2.0 message provider.
*/
@Inject
public void setOAuth2MessageProvider(final Provider<OAuth2Message> oauth2MessageProvider) {
this.oauth2MessageProvider = oauth2MessageProvider;
}
/**
* Sets the OAuth 2.0 state crypter. Used to decrypt the OAuth 2 state.
* @param stateCrypter The OAuth 2.0 state crypter.
*/
@Inject
public void setStateCrypter(@Named(OAuth2FetcherConfig.OAUTH2_STATE_CRYPTER)
final BlobCrypter stateCrypter) {
this.stateCrypter = stateCrypter;
}
}