/*
* Copyright 2012 Shared Learning Collaborative, LLC
*
* 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 org.slc.sli.dashboard.security;
import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Iterator;
import java.util.LinkedList;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.scribe.builder.ServiceBuilder;
import org.scribe.exceptions.OAuthException;
import org.scribe.model.Token;
import org.scribe.model.Verifier;
import org.scribe.oauth.OAuthService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.stereotype.Component;
import org.slc.sli.dashboard.client.APIClient;
import org.slc.sli.dashboard.client.RESTClient;
import org.slc.sli.dashboard.util.Constants;
/**
* Spring interceptor for calls that don't have a session
* This implementation simply redirects to the login URL
*
* @author dkornishev
* @author rbloh
*
*/
@Component
public class SLIAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger LOG = LoggerFactory.getLogger(SLIAuthenticationEntryPoint.class);
public static final String OAUTH_TOKEN = "OAUTH_TOKEN";
public static final String DASHBOARD_COOKIE = "SLI_DASHBOARD_COOKIE";
private static final String OAUTH_CODE = "code";
private static final String ENTRY_URL = "ENTRY_URL";
private static final String HEADER_USER_AGENT = "User-Agent";
private static final String HEADER_AJAX_INDICATOR = "X-Requested-With";
private static final String AJAX_REQUEST = "XmlHttpRequest";
// Security Utilities
private static final String NONSECURE_ENVIRONMENT_NAME = "local";
private static final String LOG_MESSAGE_AUTH_INITIATING = "Authentication: initiating {}";
private static final String LOG_MESSAGE_AUTH_VERIFYING = "Authentication: verifying {}";
private static final String LOG_MESSAGE_AUTH_COMPLETED = "Authentication: complete [{}] {}";
private static final String LOG_MESSAGE_AUTH_REDIRECTING = "Authentication: redirecting [{}] {}";
private static final String LOG_MESSAGE_AUTH_USING_COOKIE = "Authentication: using cookie {}";
private static final String LOG_MESSAGE_AUTH_EXPIRING_COOKIE = "Authentication: expiring cookie {}";
private static final String LOG_MESSAGE_AUTH_EXCEPTION = "Authentication Exception: {}";
private static final String LOG_MESSAGE_AUTH_EXCEPTION_INVALID_AUTHENTICATED = "Authentication Exception: invalid authenticated indicator";
private static final String LOG_MESSAGE_AUTH_EXCEPTION_INVALID_NAME = "Authentication Exception: invalid name indicator";
private static final String LOG_MESSAGE_AUTH_EXCEPTION_INVALID_ROLES = "Authentication Exception: invalid roles indicator";
@Value("${oauth.redirect}")
private String callbackUrl;
@Value("${api.server.url}")
private String apiUrl;
// TODO: Remove and use SDK APIClient for session checks
private RESTClient restClient;
private APIClient apiClient;
private PropertiesDecryptor propDecryptor;
public void setCallbackUrl(String callbackUrl) {
this.callbackUrl = callbackUrl;
}
public String getCallbackUrl() {
return callbackUrl;
}
public void setApiUrl(String apiUrl) {
this.apiUrl = apiUrl;
}
public String getApiUrl() {
return apiUrl;
}
public RESTClient getRestClient() {
return restClient;
}
public void setRestClient(RESTClient restClient) {
this.restClient = restClient;
}
public APIClient getApiClient() {
return apiClient;
}
public void setApiClient(APIClient apiClient) {
this.apiClient = apiClient;
}
public PropertiesDecryptor getPropDecryptor() {
return propDecryptor;
}
public void setPropDecryptor(PropertiesDecryptor propDecryptor) {
this.propDecryptor = propDecryptor;
}
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
HttpSession session = request.getSession();
try {
SliApi.setBaseUrl(apiUrl);
// Setup OAuth service
OAuthService service = new ServiceBuilder().provider(SliApi.class)
.apiKey(propDecryptor.getDecryptedClientId()).apiSecret(propDecryptor.getDecryptedClientSecret())
.callback(callbackUrl).build();
// Check cookies for token, if found insert into session
boolean cookieFound = checkCookiesForToken(request, session);
Object token = session.getAttribute(OAUTH_TOKEN);
if (token == null && request.getParameter(OAUTH_CODE) == null) {
// Initiate authentication
initiatingAuthentication(request, response, session, service);
} else if (token == null && request.getParameter(OAUTH_CODE) != null) {
// Verify authentication
verifyingAuthentication(request, response, session, service);
} else {
// Complete authentication
completeAuthentication(request, response, session, token, cookieFound);
}
} catch (OAuthException ex) {
session.invalidate();
LOG.error(LOG_MESSAGE_AUTH_EXCEPTION, new Object[] { ex.getMessage() });
response.sendError(HttpServletResponse.SC_FORBIDDEN, ex.getMessage());
return;
} catch (Exception ex) {
session.invalidate();
LOG.error(LOG_MESSAGE_AUTH_EXCEPTION, new Object[] { ex.getMessage() });
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
return;
}
}
private boolean isAjaxRequest(HttpServletRequest request) {
boolean isAjaxRequest = false;
String ajaxHeader = request.getHeader(HEADER_AJAX_INDICATOR);
if ((ajaxHeader != null) && (ajaxHeader.equalsIgnoreCase(AJAX_REQUEST))) {
isAjaxRequest = true;
}
return isAjaxRequest;
}
private boolean checkCookiesForToken(HttpServletRequest request, HttpSession session) {
boolean cookieFound = false;
// If there is no oauth credential, and the user has a dashboard cookie, add cookie value as
// oauth session attribute.
if (session.getAttribute(OAUTH_TOKEN) == null) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
// Loop through cookies to find dashboard cookie
for (Cookie c : cookies) {
if (c.getName().equals(DASHBOARD_COOKIE)) {
// DE883. We need to decrypt the cookie value to authenticate the token.
String decryptedCookie = null;
try {
String s = URLDecoder.decode(c.getValue(), "UTF-8");
decryptedCookie = propDecryptor.decrypt(s);
} catch (Exception e) {
LOG.error(e.getMessage());
}
JsonObject json = restClient.sessionCheck(decryptedCookie);
// If user is not authenticated, expire the cookie, else set OAUTH_TOKEN to
// cookie value and continue
JsonElement authElement = json.get(Constants.ATTR_AUTHENTICATED);
if ((authElement != null) && (!authElement.getAsBoolean())) {
c.setMaxAge(0);
LOG.info(LOG_MESSAGE_AUTH_EXPIRING_COOKIE, new Object[] { request.getRemoteAddr() });
} else {
cookieFound = true;
session.setAttribute(OAUTH_TOKEN, decryptedCookie);
LOG.info(LOG_MESSAGE_AUTH_USING_COOKIE, new Object[] { request.getRemoteAddr() });
}
}
}
}
}
return cookieFound;
}
private void saveCookieWithToken(HttpServletRequest request, HttpServletResponse response, String token) {
// DE476 Using custom header, since servlet api version 2.5 does not support
// httpOnly
// TODO: Remove custom header and use cookie when servlet-api is upgraded to 3.0
// response.setHeader("Set-Cookie", DASHBOARD_COOKIE + "=" + (String) token +
// ";path=/;domain=" + domain of the request + ";Secure;HttpOnly");
String encryptedToken = null;
String headerString = "";
// DE883 Encrypt the cookie and save it in the header.
try {
encryptedToken = propDecryptor.encrypt(token);
headerString = DASHBOARD_COOKIE + "=" + URLEncoder.encode(encryptedToken, "UTF-8") + ";path=/;domain="
+ request.getServerName() + ";HttpOnly";
if (isSecureRequest(request)) {
headerString = headerString + (";Secure");
}
} catch (Exception e) {
LOG.error(e.getMessage());
}
response.setHeader("Set-Cookie", headerString);
}
private void initiatingAuthentication(HttpServletRequest request, HttpServletResponse response,
HttpSession session, OAuthService service) throws IOException {
LOG.info(LOG_MESSAGE_AUTH_INITIATING, new Object[] { request.getRemoteAddr() });
session.setAttribute(ENTRY_URL, request.getRequestURL());
// The request token doesn't matter for OAuth 2.0 which is why it's null
String authUrl = service.getAuthorizationUrl(null);
response.sendRedirect(authUrl);
}
private void verifyingAuthentication(HttpServletRequest request, HttpServletResponse response, HttpSession session,
OAuthService service) throws IOException {
LOG.info(LOG_MESSAGE_AUTH_VERIFYING, new Object[] { request.getRemoteAddr() });
Verifier verifier = new Verifier(request.getParameter(OAUTH_CODE));
Token accessToken = service.getAccessToken(null, verifier);
session.setAttribute(OAUTH_TOKEN, accessToken.getToken());
Object entryUrl = session.getAttribute(ENTRY_URL);
if (entryUrl != null) {
response.sendRedirect(session.getAttribute(ENTRY_URL).toString());
} else {
response.sendRedirect(request.getRequestURI());
}
}
private void completeAuthentication(HttpServletRequest request, HttpServletResponse response, HttpSession session,
Object token, boolean cookieFound) throws ServletException, IOException {
// Complete Spring security integration
SLIPrincipal principal = completeSpringAuthentication((String) token);
LOG.info(LOG_MESSAGE_AUTH_COMPLETED, new Object[] { principal.getName(), request.getRemoteAddr() });
// Save the cookie to support sessions across multiple dashboard servers
saveCookieWithToken(request, response, (String) token);
// AJAX calls OR cookie sessions should not redirect
if (isAjaxRequest(request) || cookieFound) {
RequestDispatcher dispatcher = request.getRequestDispatcher(request.getServletPath());
dispatcher.forward(request, response);
} else {
LOG.info(LOG_MESSAGE_AUTH_REDIRECTING, new Object[] { principal.getName(), request.getRemoteAddr() });
response.sendRedirect(request.getRequestURI());
}
}
private SLIPrincipal completeSpringAuthentication(String token) {
// Get authentication information
JsonObject json = restClient.sessionCheck(token);
LOG.debug(json.toString());
// If the user is authenticated, create an SLI principal, and authenticate
JsonElement authElement = json.get(Constants.ATTR_AUTHENTICATED);
if ((authElement != null) && (authElement.getAsBoolean())) {
// Setup principal
SLIPrincipal principal = new SLIPrincipal();
principal.setId(token);
// Extract user name from authentication payload
String username = "";
JsonElement nameElement = json.get(Constants.ATTR_AUTH_FULL_NAME);
if (nameElement != null) {
username = nameElement.getAsString();
if (username != null && username.contains("@")) {
username = username.substring(0, username.indexOf("@"));
if (username.contains(".")) {
String first = username.substring(0, username.indexOf('.'));
String second = username.substring(username.indexOf('.') + 1);
username = first.substring(0, 1).toUpperCase()
+ (first.length() > 1 ? first.substring(1) : "")
+ (second.substring(0, 1).toUpperCase() + (second.length() > 1 ? second.substring(1)
: ""));
}
}
} else {
LOG.error(LOG_MESSAGE_AUTH_EXCEPTION_INVALID_NAME);
}
// Set principal name
principal.setName(username);
// Extract user roles from authentication payload
LinkedList<GrantedAuthority> authList = new LinkedList<GrantedAuthority>();
JsonArray grantedAuthorities = json.getAsJsonArray(Constants.ATTR_AUTH_ROLES);
if (grantedAuthorities != null) {
// Add authorities to user principal
Iterator<JsonElement> authIterator = grantedAuthorities.iterator();
while (authIterator.hasNext()) {
JsonElement nextElement = authIterator.next();
authList.add(new GrantedAuthorityImpl(nextElement.getAsString()));
}
} else {
LOG.error(LOG_MESSAGE_AUTH_EXCEPTION_INVALID_ROLES);
}
SecurityContextHolder.getContext().setAuthentication(
new PreAuthenticatedAuthenticationToken(principal, token, authList));
return principal;
} else {
LOG.error(LOG_MESSAGE_AUTH_EXCEPTION_INVALID_AUTHENTICATED);
}
return null;
}
/**
* @param request
* - request to be determined
* @return if the ENV is non-local ( this is due to local jetty server does not
* handle secure protocol ) and the protocol is HTTPS, return true.
* Otherwise, return false.
*/
static boolean isSecureRequest(HttpServletRequest request) {
String serverName = request.getServerName();
boolean isSecureEnvironment = (!serverName.substring(0, serverName.indexOf(".")).equals(
NONSECURE_ENVIRONMENT_NAME));
return (request.isSecure() && isSecureEnvironment);
}
}