/*******************************************************************************
* Copyright (c) 2012, 2014 IBM Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Eclipse Distribution License v. 1.0 which accompanies this distribution.
*
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
*
* Michael Fiedler - initial API and implementation for Bugzilla adapter
* Susumu Fukuda - extracted this class from Bugzilla adapter CredentialsFilter
* Samuel Padgett - fix NPEx when Exception.getCause() returns null in Application.login()
*******************************************************************************/
package org.eclipse.lyo.server.oauth.core.utils;
import java.io.IOException;
import java.net.URISyntaxException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import net.oauth.OAuth;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthException;
import net.oauth.OAuthMessage;
import net.oauth.OAuthProblemException;
import net.oauth.http.HttpMessage;
import net.oauth.server.OAuthServlet;
import org.eclipse.lyo.server.oauth.core.Application;
import org.eclipse.lyo.server.oauth.core.AuthenticationException;
import org.eclipse.lyo.server.oauth.core.OAuthConfiguration;
import org.eclipse.lyo.server.oauth.core.OAuthRequest;
import org.eclipse.lyo.server.oauth.core.consumer.ConsumerStore;
import org.eclipse.lyo.server.oauth.core.consumer.LyoOAuthConsumer;
import org.eclipse.lyo.server.oauth.core.token.LRUCache;
import org.eclipse.lyo.server.oauth.core.token.SimpleTokenStrategy;
/**
* <h3>Overview</h3>
* Purpose: Provide a JEE Servlet filter base implementation for accepting
* both HTTP basic and OAuth provider authentication, connecting your tool using the
* credentials, and managing the connections.
*
* <p>With this credentitals filter:<ul>
* <li>Your Webapp can accepts HTTP Basic authentication
* <li>Your Webapp can works as an OAuth provider
* </ul>
* <p>Once user entered credentials via HTTP Basic auth or OAuth, it
* is passed to a callback method {@link #getCredentialsFromRequest(HttpServletRequest)}
* or {@link #getCredentialsForOAuth(String, String)} so that your implementation
* can build a Credentials object from the given data.
* And then, next callback method {@link #login(Object, HttpServletRequest)} is invoked for
* authenticate the credentials and building connection to your back-end tool.
* Concrete types of the credentials and the connection can be specified as type
* parameters of this class.
*
* <p>While processing a request, the credentials and the connection are available
* as attributes of the request. Your subsequent process such as {@link HttpServlet#service(ServletRequest, ServletResponse)}
* can extract and use them for accessing your tool. You can use {@link #getConnector(HttpServletRequest)}
* and {@link #getCredentials(HttpServletRequest)} to retrieve them from the request.
*
* <h3>Usage</h3>
* <p>You have to subclass this class and give implementations for the following methods:
* <ul>
* <li>{@link #login(Object, HttpServletRequest)}
* <li>{@link #getCredentialsFromRequest(HttpServletRequest)}
* <li>{@link #getCredentialsForOAuth(String, String)}
* <li>{@link #isAdminSession(String, Object, HttpServletRequest)}
* <li>{@link #createConsumerStore()}
* <li>{@link #logout(Object, HttpSession)} (optional)
* </ul>
* Then, add the follwoing filter-mapping to your web.xml:
* <pre>
* <filter>
* <display-name>[YOUR FILTER CLASS NAME (MyFilter)]</display-name>
* <filter-name>[YOUR FILTER CLASS NAME (MyFilter)]</filter-name>
* <filter-class>[FULLY QUALIFIED YOUR FILTER CLASS NAME (com.example.MyFilter)]</filter-class>
* </filter>
* <filter-mapping>
* <filter-name>[YOUR FILTER CLASS NAME (MyFilter)]</filter-name>
* <url-pattern>/services/*</url-pattern>
* </filter-mapping>
* </pre>
*
* @param <Connection> Type for connection object to your tool
* @param <Credentials> Type for credentials for your tool. (e.g. UsernamePasswordCredentials)
*/
abstract public class AbstractAdapterCredentialsFilter<Credentials, Connection> implements Filter {
private static final String ATTRIBUTE_BASE = "org.eclipse.lyo.server.oauth.core.utils.";
public static final String CONNECTOR_ATTRIBUTE = ATTRIBUTE_BASE + "Connector";
public static final String CREDENTIALS_ATTRIBUTE = ATTRIBUTE_BASE + "Credentials";
public static final String ADMIN_SESSION_ATTRIBUTE = ATTRIBUTE_BASE + "AdminSession";
public static final String JAZZ_INVALID_EXPIRED_TOKEN_OAUTH_PROBLEM = "invalid_expired_token";
public static final String OAUTH_EMPTY_TOKEN_KEY = new String("OAUTH_EMPTY_TOKEN_KEY");
private final LRUCache<String, Connection> tokenToConnectionCache = new LRUCache<String, Connection>(200);
final private String displayName;
final private String realm;
/**
* Constructor
* @param displayName application name displayed on the login prompt
* @param realm realm for this adapter
*/
protected AbstractAdapterCredentialsFilter(String displayName, String realm) {
this.displayName = displayName;
this.realm = realm;
}
/**
* Extract credentials from the request and return it.
* @param request {@link HttpServletRequest}
* @return credentials
* @throws UnauthorizedException iff no login credentials associated to the request.
*/
abstract protected Credentials getCredentialsFromRequest(HttpServletRequest request) throws UnauthorizedException;
/**
* Create a Credentials object from given user id and password.
*
* <p>For OAuth two-legged request, the <code>id</code> is set to {@link #OAUTH_EMPTY_TOKEN_KEY}
* object. Implementor can compare the value using <code>==</code> to identify the request.
* In the request the consumer key is set to the <code>password</code>. So you might find a functional
* user associated to the consumer key with the value.
* @param id user id or {@link #OAUTH_EMPTY_TOKEN_KEY}
* @param password password or OAuth consumer key
* @return credentials
*/
abstract protected Credentials getCredentialsForOAuth(String id, String password);
/**
* Create connection to your tool using the given credentials, and returns the connection.
* @param crdentials credentials for login
* @param request {@link HttpServletRequest}
* @return connection that represents the successful login session
* @throws UnauthorizedException credentials is invalid
* @throws ServletException other exceptional situation
*/
abstract protected Connection login(Credentials crdentials, HttpServletRequest request) throws UnauthorizedException, ServletException;
/**
* Logout
* @param loginSession
* @param session
*/
protected void logout(Connection loginSession, HttpSession session) {
// do nothing by default
}
/**
* Tell if this is an admin session. For admin session, Lyo provides user-interface to
* accept provisional authentication key.
* @param id
* @param session
* @param request
* @return
*/
abstract protected boolean isAdminSession(String id, Connection session, HttpServletRequest request);
/**
* Invoked from this class to create {@link ConsumerStore} for OAuth keys.
* Typical implementation can be:
* <pre>return new FileSystemConsumerStore("YourOAuthStore.xml");
* </pre>
* @return
* @throws Exception
*/
abstract protected ConsumerStore createConsumerStore() throws Exception;
/**
* get Connector assigned to this request
*
* The connector should be placed in the session by the CredentialsFilter servlet filter
*
* @param request
* @return connector
*/
public static <T> T getConnector(HttpServletRequest request)
{
//connector should never be null if CredentialsFilter is doing its job
@SuppressWarnings("unchecked")
T connector = (T) request.getAttribute(CONNECTOR_ATTRIBUTE);
return connector;
}
/**
* Get Credentials for this session
* @param request
* @return credentials
*/
public static <T> T getCredentials(HttpServletRequest request)
{
@SuppressWarnings("unchecked")
T credentials = (T) request.getSession().getAttribute(CREDENTIALS_ATTRIBUTE);
return credentials;
}
protected String getOAuthRealm() {
return realm;
}
protected String getDisplayName() {
return displayName;
}
@Override
public void destroy() {
}
/**
* Check for OAuth or BasicAuth credentials and challenge if not found.
*
* Store the Connector in the HttpSession for retrieval in the REST services.
*/
@SuppressWarnings("unchecked")
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain chain) throws IOException, ServletException {
if(servletRequest instanceof HttpServletRequest && servletResponse instanceof HttpServletResponse) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
boolean isTwoLeggedOAuthRequest = false;
String twoLeggedOAuthConsumerKey = null;
//Don't protect requests to oauth service. TODO: possibly do this in web.xml
if (! request.getPathInfo().startsWith("/oauth"))
{
// First check if this is an OAuth request.
try {
try {
OAuthMessage message = OAuthServlet.getMessage(request, null);
// test if this is a valid two-legged oauth request
if ("".equals(message.getToken())) {
validateTwoLeggedOAuthMessage(message);
isTwoLeggedOAuthRequest = true;
twoLeggedOAuthConsumerKey = message.getConsumerKey();
}
if (!isTwoLeggedOAuthRequest && message.getToken() != null) {
OAuthRequest oAuthRequest = new OAuthRequest(request);
oAuthRequest.validate();
Connection connector = tokenToConnectionCache.get(message.getToken());
if (connector == null) {
throw new OAuthProblemException(
OAuth.Problems.TOKEN_REJECTED);
}
request.getSession().setAttribute(CONNECTOR_ATTRIBUTE, connector);
}
} catch (OAuthProblemException e) {
if (OAuth.Problems.TOKEN_REJECTED.equals(e.getProblem()))
throwInvalidExpiredException(e);
else
throw e;
}
} catch (OAuthException e) {
OAuthServlet.handleException(response, e, getOAuthRealm());
return;
} catch (URISyntaxException e) {
throw new ServletException(e);
}
// Check for Basic authentication if this is not an OAuth request
HttpSession session = request.getSession();
Connection connector = (Connection) session.getAttribute(CONNECTOR_ATTRIBUTE);
if (connector == null) {
try {
Credentials credentials;
if (isTwoLeggedOAuthRequest) {
connector = tokenToConnectionCache.get("");
if (connector == null) {
credentials = getCredentialsForOAuth(OAUTH_EMPTY_TOKEN_KEY, twoLeggedOAuthConsumerKey);
connector = login(credentials, request);
tokenToConnectionCache.put("", connector);
}
credentials = null; // TODO; Do we need to keep the credentials for this path ??
} else {
credentials = (Credentials) request.getSession().getAttribute(CREDENTIALS_ATTRIBUTE);
if (credentials == null)
{
credentials = getCredentialsFromRequest(request);
if (credentials == null) {
throw new UnauthorizedException();
}
}
connector = login(credentials, request);
}
session.setAttribute(CONNECTOR_ATTRIBUTE, connector);
session.setAttribute(CREDENTIALS_ATTRIBUTE, credentials);
} catch (UnauthorizedException e)
{
sendUnauthorizedResponse(response, e);
System.err.println(e.getMessage());
return;
} catch (ServletException ce)
{
throw ce;
}
}
if (connector != null) {
doChainDoFilterWithConnector(request, response, chain, connector);
return;
}
}
}
chain.doFilter(servletRequest, servletResponse);
}
/**
* The default implementation is:
* <pre>
* request.setAttribute(CONNECTOR_ATTRIBUTE, connector);
* chain.doFilter(request, response);</pre>
*
* Subclass may invoke the <code>chain.doFilter()</code> directly instead of invoking super method.
*
* @param request {@link HttpServletRequest}
* @param response {@link HttpServletResponse}
* @param chain {@link FilterChain}
* @param sessionConnector {@link Connector} to be used for processing rest of the chain (i.e. REST request)
* @throws IOException
* @throws ServletException
*/
protected void doChainDoFilterWithConnector(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Connection connector) throws IOException, ServletException {
request.setAttribute(CONNECTOR_ATTRIBUTE, connector);
chain.doFilter(request, response);
}
private void validateTwoLeggedOAuthMessage(OAuthMessage message)
throws IOException, OAuthException,
URISyntaxException {
OAuthConfiguration config = OAuthConfiguration.getInstance();
LyoOAuthConsumer consumer = config.getConsumerStore().getConsumer(message.getConsumerKey());
if (consumer != null && consumer.isTrusted()) {
// The request can be a two-legged oauth request because it's a trusted consumer
// Validate the message with an empty token and an empty secret
OAuthAccessor accessor = new OAuthAccessor(consumer);
accessor.requestToken = "";
accessor.tokenSecret = "";
config.getValidator().validateMessage(message, accessor);
} else {
throw new OAuthProblemException(
OAuth.Problems.TOKEN_REJECTED);
}
}
private HttpSessionListener listener = new HttpSessionListener() {
@Override
public void sessionDestroyed(HttpSessionEvent se) {
HttpSession session = se.getSession();
@SuppressWarnings("unchecked")
Connection loginSession = (Connection) session.getAttribute(CONNECTOR_ATTRIBUTE);
logout(loginSession, session);
}
@Override
public void sessionCreated(HttpSessionEvent se) {
// nothing
}
};
@Override
public void init(FilterConfig filterConfig) throws ServletException {
OAuthConfiguration config = OAuthConfiguration.getInstance();
// Add session listener
filterConfig.getServletContext().addListener(listener);
// Validates a user's ID and password.
config.setApplication(new Application() {
@Override
public void login(HttpServletRequest request, String id,
String password) throws AuthenticationException {
try {
Credentials creds = getCredentialsForOAuth(id, password);
request.getSession().setAttribute(CREDENTIALS_ATTRIBUTE, creds);
Connection c = AbstractAdapterCredentialsFilter.this.login(creds, request);
request.setAttribute(CONNECTOR_ATTRIBUTE, c);
boolean isAdmin = AbstractAdapterCredentialsFilter.this.isAdminSession(id, c, request);
request.getSession().setAttribute(ADMIN_SESSION_ATTRIBUTE, isAdmin);
} catch (Exception e) {
if (e.getCause() != null) {
throw new AuthenticationException(e.getCause().getMessage(), e);
} else {
throw new AuthenticationException(e);
}
}
}
@Override
public String getName() {
// Display name for this application.
return getDisplayName();
}
@Override
public boolean isAdminSession(HttpServletRequest request) {
return Boolean.TRUE.equals(request.getSession().getAttribute(
ADMIN_SESSION_ATTRIBUTE));
}
@Override
public String getRealm(HttpServletRequest request) {
return getOAuthRealm();
}
@Override
public boolean isAuthenticated(HttpServletRequest request) {
@SuppressWarnings("unchecked")
Connection connector = (Connection) request.getSession().getAttribute(CONNECTOR_ATTRIBUTE);
if (connector == null) {
return false;
}
request.setAttribute(CONNECTOR_ATTRIBUTE, connector);
return true;
}
});
/*
* Override some SimpleTokenStrategy methods so that we can keep the
* Connector associated with the OAuth tokens.
*/
config.setTokenStrategy(new SimpleTokenStrategy() {
@SuppressWarnings("unchecked")
@Override
public void markRequestTokenAuthorized(
HttpServletRequest httpRequest, String requestToken)
throws OAuthProblemException {
tokenToConnectionCache.put(requestToken,
(Connection) httpRequest.getAttribute(CONNECTOR_ATTRIBUTE));
super.markRequestTokenAuthorized(httpRequest, requestToken);
}
@Override
public void generateAccessToken(OAuthRequest oAuthRequest)
throws OAuthProblemException, IOException {
String requestToken = oAuthRequest.getMessage().getToken();
Connection bc = tokenToConnectionCache.remove(requestToken);
super.generateAccessToken(oAuthRequest);
tokenToConnectionCache.put(oAuthRequest.getAccessor().accessToken, bc);
}
});
try {
// For now, hard-code the consumers.
config.setConsumerStore(createConsumerStore());
} catch (Throwable t) {
System.err.println("Error initializing the OAuth consumer store: " + t.getMessage());
}
}
/**
* Jazz requires a exception with the magic string "invalid_expired_token" to restart
* OAuth authentication
* @param e
* @return
* @throws OAuthProblemException
*/
private void throwInvalidExpiredException(OAuthProblemException e) throws OAuthProblemException {
OAuthProblemException ope = new OAuthProblemException(JAZZ_INVALID_EXPIRED_TOKEN_OAUTH_PROBLEM);
ope.setParameter(HttpMessage.STATUS_CODE, new Integer(
HttpServletResponse.SC_UNAUTHORIZED));
throw ope;
}
private void sendUnauthorizedResponse(HttpServletResponse response,
UnauthorizedException e) throws IOException, ServletException {
// Accept basic access or OAuth authentication.
final String WWW_AUTHENTICATE_HEADER = "WWW-Authenticate";
final String BASIC_AUTHORIZATION_PREFIX = "Basic ";
final String BASIC_AUTHENTICATION_CHALLENGE = BASIC_AUTHORIZATION_PREFIX
+ "realm=\"" + getOAuthRealm() + "\"";
final String OAUTH_AUTHORIZATION_PREFIX = "OAuth ";
final String OAUTH_AUTHENTICATION_CHALLENGE = OAUTH_AUTHORIZATION_PREFIX
+ "realm=\"" + getOAuthRealm() + "\"";
response.addHeader(WWW_AUTHENTICATE_HEADER,
OAUTH_AUTHENTICATION_CHALLENGE);
response.addHeader(WWW_AUTHENTICATE_HEADER,
BASIC_AUTHENTICATION_CHALLENGE);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}