/* Copyright (c) 2006 Google 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 sample.gbase.recipe;
import com.google.api.gbase.client.FeedURLFactory;
import com.google.api.gbase.client.GoogleBaseService;
import com.google.gdata.client.http.AuthSubUtil;
import com.google.gdata.util.AuthenticationException;
import java.io.IOException;
import java.net.URL;
import java.net.MalformedURLException;
import java.security.GeneralSecurityException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Provides an authenticated GoogleBaseService
* to the servlet that will process the request.
*/
public class AuthenticationFilter implements Filter {
/**
* The request attribute that contains the authenticated service.
*/
static final String SERVICE_ATTRIBUTE = "googleBaseService";
/**
* The parameter used by AuthSub to specify the authentication token.
* Also used as the name of the http session attribute that contains
* the session token.
*/
static final String TOKEN_PARAMETER = "token";
static final String TOKEN_COOKIE_NAME = "AuthSubSessionToken";
static final String DEFAULT_AUTHSUB_PROTOCOL = "https";
static final String DEFAULT_AUTHSUB_HOSTNAME = "www.google.com";
protected String authsubProtocol;
protected String authsubHostname;
protected FeedURLFactory urlFactory;
protected String applicationName;
/**
* Developer key, used for identification against the Google Base data
* API servers.
*/
protected String key;
private ServletContext servletContext;
public void init(FilterConfig filterConfig) throws ServletException {
servletContext = filterConfig.getServletContext();
key = servletContext.getInitParameter(RecipeUtil.DEVELOPER_KEY_PARAMETER);
if (key == null || "".equals(key.trim())) {
String errorMessage = "No developer key specified.\n Please edit " +
"web.xml and add your developer key in the \"key\" context " +
"parameter. \n You can obtain a developer key at: \n\t" +
"http://code.google.com/api/base/signup.html";
System.err.println(errorMessage);
throw new ServletException(errorMessage);
}
urlFactory = (FeedURLFactory)
servletContext.getAttribute(RecipeListener.FEED_URL_FACTORY_ATTRIBUTE);
authsubProtocol = filterConfig.getInitParameter("authsubProtocol");
if (authsubProtocol == null) {
authsubProtocol = DEFAULT_AUTHSUB_PROTOCOL;
}
authsubHostname = filterConfig.getInitParameter("authsubHostname");
if (authsubHostname == null) {
authsubHostname = DEFAULT_AUTHSUB_HOSTNAME;
}
}
public void destroy() {
servletContext = null;
urlFactory = null;
applicationName = null;
authsubProtocol = null;
authsubHostname = null;
}
/**
* Starts or stops an authenticated session, depending on the value
* of the {@value #TOKEN_PARAMETER} parameter, or provides the servlets
* with an authenticated
* {@link com.google.api.gbase.client.GoogleBaseService GoogleBaseService},
* during an authenticated session.
*/
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String oneTimeToken = httpRequest.getParameter(TOKEN_PARAMETER);
String sessionToken = getSessionTokenCookie(httpRequest);
if (oneTimeToken != null) {
if ("".equals(oneTimeToken)) {
// Revoke the session token
if (sessionToken != null) {
stopAuthenticatedSession(httpRequest, httpResponse, sessionToken);
return;
}
} else {
// Convert the token to a session token and keep it
try {
startAuthenticatedSession(httpRequest, httpResponse, oneTimeToken);
return;
} catch (GeneralSecurityException e) {
throw new ServletException(e);
} catch (AuthenticationException e) {
// Log and then continue as if this token was not there.
// (It was probably bookmarked.)
servletContext.log("Invalid one-time token", e);
}
}
}
// Request a new token if we don't have one at this point
if (sessionToken == null) {
redirectToAuthSub(httpRequest, httpResponse);
return;
}
// Create a service that authenticates using the session token
GoogleBaseService service = new GoogleBaseService(
applicationName, key, authsubProtocol, authsubHostname);
service.setAuthSubToken(sessionToken);
// Make the service available to the servlet
httpRequest.setAttribute(SERVICE_ATTRIBUTE, service);
// Execute the servlet
try {
filterChain.doFilter(request, response);
} catch(ServletException e) {
Throwable cause = e.getRootCause();
if (cause instanceof AuthenticationException &&
!response.isCommitted()) {
// Token has been revoked. Re-run AuthSub.
redirectToAuthSub(httpRequest, httpResponse);
} else {
// Let the exception be handled as usual.
throw e;
}
}
}
/**
* Gets the AuthSub session token from a cookie.
*
* @param httpRequest
* @return session token or null
*/
String getSessionTokenCookie(HttpServletRequest httpRequest) {
Cookie[] cookies = httpRequest.getCookies();
if (cookies == null) {
return null;
}
for (Cookie cookie : cookies) {
if (cookie.getName().equals(TOKEN_COOKIE_NAME)) {
return cookie.getValue();
}
}
return null;
}
/**
* Revokes the specified session token and redirects the browser to
* the context of the request.
*
* @param request
* @param response
* @param sessionToken a session token
* @throws IOException
* @throws ServletException
*/
private void stopAuthenticatedSession(HttpServletRequest request,
HttpServletResponse response,
String sessionToken)
throws IOException, ServletException {
if (sessionToken != null) {
revokeSessionToken(sessionToken);
clearSessionTokenCookie(request, response);
}
String url = request.getContextPath();
response.sendRedirect(response.encodeRedirectURL(url));
}
/**
* Force cookie expiration by sending an expired cookie to
* the browser.
*
* @param request
* @param response
*/
private void clearSessionTokenCookie(HttpServletRequest request,
HttpServletResponse response)
throws ServletException {
response.addCookie(newExpiredSessionTokenCookie(request));
}
private Cookie newExpiredSessionTokenCookie(HttpServletRequest request)
throws ServletException {
Cookie cookie = newSessionTokenCookie(request, "");
cookie.setMaxAge(0);
return cookie;
}
/**
* Explicitely revoke the session token.
*
* @param token
* @throws IOException
* @throws ServletException
*/
protected void revokeSessionToken(String token) throws IOException, ServletException
{
try {
AuthSubUtil.revokeToken(authsubProtocol, authsubHostname, token, null);
} catch (AuthenticationException e) {
throw new ServletException(e);
} catch (GeneralSecurityException e) {
throw new ServletException(e);
}
}
/**
* Exchanges the single use token for a session token and
* redirects the browser to the same address with the
* token specification part removed.
*
* @param request
* @param response
* @param oneTimeToken a single use AuthSub token
* @throws IOException
* @throws GeneralSecurityException
* @throws AuthenticationException if the one-time token is invalid
*/
private void startAuthenticatedSession(HttpServletRequest request,
HttpServletResponse response,
String oneTimeToken)
throws IOException, GeneralSecurityException, AuthenticationException,
ServletException {
String sessionToken = exchangeForSessionToken(oneTimeToken);
// Store the authentication token in a cookie
response.addCookie(newSessionTokenCookie(request, sessionToken));
// Redirect the browser to the same address
// with the "token=value" part removed from the query string
StringBuffer url = request.getRequestURL();
String queryString = request.getQueryString();
if (queryString != null) {
queryString = queryString.replaceFirst("token=[^&]*&?", "");
if (queryString.length() > 0) {
url.append("?").append(queryString);
}
}
response.sendRedirect(response.encodeRedirectURL(url.toString()));
}
protected Cookie newSessionTokenCookie(HttpServletRequest request,
String sessionToken)
throws ServletException {
Cookie cookie = new Cookie(TOKEN_COOKIE_NAME, sessionToken);
// AuthSub session tokens effectively don't expire. Hang on to it.
// If the AuthSub token is revoked, the filter will request
// a new token.
cookie.setMaxAge(365*24*60*60);
cookie.setPath(request.getContextPath() + "/");
try {
cookie.setDomain(new URL(request.getRequestURL().toString()).getHost());
} catch(MalformedURLException e) {
throw new ServletException(e);
}
// Cookie domain set automatically by the server.
return cookie;
}
/**
* Converts a one-time token into a reusable session token.
* @param oneTimeToken
* @throws IOException
*/
protected String exchangeForSessionToken(String oneTimeToken)
throws IOException, GeneralSecurityException, AuthenticationException {
return AuthSubUtil.exchangeForSessionToken(authsubProtocol,
authsubHostname,
oneTimeToken,
null);
}
/**
* Redirects to the AuthSub authentication page.
*
* @param request
* @param response
* @throws IOException
*/
private void redirectToAuthSub(HttpServletRequest request,
HttpServletResponse response)
throws IOException {
StringBuffer next = request.getRequestURL();
String queryString = request.getQueryString();
if (queryString != null && !"".equals(queryString) ) {
next.append("?").append(queryString);
}
String scope = new URL(urlFactory.getBaseURL(), "feeds").toExternalForm();
String url = AuthSubUtil.getRequestUrl(authsubProtocol,
authsubHostname,
next.toString(),
scope,
false,
true);
response.sendRedirect(response.encodeRedirectURL(url));
}
}