/**
* Copyright (c) 2013 Mark S. Kolich
* http://mark.koli.ch
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
package com.kolich.havalo.filters;
import com.kolich.curacao.annotations.Injectable;
import com.kolich.curacao.handlers.requests.CuracaoRequestContext;
import com.kolich.curacao.handlers.requests.filters.CuracaoRequestFilter;
import com.kolich.havalo.components.RepositoryManagerComponent;
import com.kolich.havalo.entities.types.KeyPair;
import com.kolich.havalo.exceptions.authentication.AuthenticationException;
import com.kolich.havalo.exceptions.authentication.BadCredentialsException;
import org.slf4j.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
import static com.google.common.net.HttpHeaders.*;
import static org.apache.commons.codec.binary.Base64.encodeBase64;
import static org.apache.commons.codec.binary.StringUtils.getBytesUtf8;
import static org.apache.commons.codec.binary.StringUtils.newStringUtf8;
import static org.apache.commons.io.IOUtils.LINE_SEPARATOR_UNIX;
import static org.slf4j.LoggerFactory.getLogger;
public final class HavaloAuthenticationFilter implements CuracaoRequestFilter {
private static final Logger logger__ =
getLogger(HavaloAuthenticationFilter.class);
public static final String HAVALO_AUTHENTICATION_ATTRIBUTE =
"havalo.authentication";
private static final String HAVALO_AUTHORIZATION_PREFIX = "Havalo ";
private static final String HAVALO_AUTHORIZATION_SEPARATOR = ":";
private final HavaloUserService userService_;
@Injectable
public HavaloAuthenticationFilter(final RepositoryManagerComponent component) {
userService_ = new HavaloUserService(component.getRepositoryManager());
}
@Override
public final void filter(final CuracaoRequestContext context) throws Exception {
try {
final HttpServletRequest request = context.request_;
// Extract the Authorization header from the incoming HTTP request.
String header = request.getHeader(AUTHORIZATION);
// If the header does not exist or does not start with the correct
// token, give up immediately.
if(header == null || !header.startsWith(HAVALO_AUTHORIZATION_PREFIX)) {
throw new AuthenticationException("Request did not contain " +
"a valid '" + AUTHORIZATION + "' header.");
}
// Extract just the part of the Authorization header that follows
// the Havalo authorization prefix.
header = header.substring(HAVALO_AUTHORIZATION_PREFIX.length());
final String[] tokens = header.split(HAVALO_AUTHORIZATION_SEPARATOR, 2);
if(tokens == null || tokens.length != 2) {
throw new AuthenticationException("Failed to extract correct " +
"number of tokens from '" + AUTHORIZATION + "' header.");
}
// If we get here, then we must have had some valid input
// Authorization header with a real access key and signature.
final String accessKey = tokens[0], signature = tokens[1];
// request.getRequestURI();
// Extract username from incoming signed request header.
// Expected format is ...
// Authorization: Havalo AccessKey:Signature
// ... where the AccessKey is the unique UUID used to identify the user.
// And, the Signature is ...
// Base64( HMAC-SHA256( UTF-8-Encoding-Of( AccessSecret, StringToSign ) ) );
// And, the StringToSign is ....
// HTTP-Verb (GET, PUT, POST, or DELETE) + "\n" +
// RFC822 Date (from 'Date' request header, must exist) + "\n" +
// Content-Type (from 'Content-Type' request header, optional) + "\n" +
// CanonicalizedResource (the part of this request's URL from
// the protocol name up to the query string in the first line
// of the HTTP request)
// Call the user details service to load the user data for the UUID.
final KeyPair userKp = userService_.loadKeyPairById(
UUID.fromString(accessKey));
if(userKp == null) {
throw new AuthenticationException("User service returned " +
"null, which is an interface contract violation.");
}
// Get the string to sign -- will fail gracefully if the incoming
// request does not have the proper headers attached to it.
final String stringToSign = getStringToSign(request);
// Compute the resulting signed signature.
final String computed = HMACSHA256Signer.sign(userKp, stringToSign);
// Does the signature match what was passed to us in the
// Authorization request header?
if(!computed.equals(signature)) {
throw new BadCredentialsException("Signatures did not " +
"match (request=" + signature + ", computed=" + computed +
")");
}
// Success!
request.setAttribute(HAVALO_AUTHENTICATION_ATTRIBUTE, userKp);
} catch (Exception e) {
logger__.debug("Authentication failure; service failed " +
"to authenticate request.", e);
throw new AuthenticationException("Authentication " +
"failed; either the provided signature did not match, " +
"or you do not have permission to access the requested " +
"resource.", e);
}
}
private static final String getStringToSign(final HttpServletRequest request) {
final StringBuilder sb = new StringBuilder();
// HTTP-Verb (GET, PUT, POST, or DELETE) + "\n"
sb.append(request.getMethod().toUpperCase()).append(LINE_SEPARATOR_UNIX);
// RFC822 Date (from 'Date' request header, must exist) + "\n" +
final String dateHeader;
if((dateHeader = request.getHeader(DATE)) == null) {
throw new BadCredentialsException("Incoming request missing " +
"required '" + DATE + "' request header.");
}
sb.append(dateHeader).append(LINE_SEPARATOR_UNIX);
// Content-Type (from 'Content-Type' request header, optional) + "\n" +
final String contentType;
if((contentType = request.getHeader(CONTENT_TYPE)) != null) {
sb.append(contentType);
}
sb.append(LINE_SEPARATOR_UNIX);
// CanonicalizedResource
sb.append(request.getRequestURI());
return sb.toString();
}
/**
* Computes an HMAC-SHA256 signature.
*/
private static final class HMACSHA256Signer {
private static final String HMAC_SHA256_ALGORITHM_NAME = "HmacSHA256";
/**
* Returns a Base-64 encoded HMAC-SHA256 signature.
*/
public static final String sign(final KeyPair kp,
final String input) {
try {
// Get a new instance of the HMAC-SHA256 algorithm.
final Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM_NAME);
// Init it with our secret and the secret-key algorithm.
mac.init(new SecretKeySpec(getBytesUtf8(kp.getSecret()),
HMAC_SHA256_ALGORITHM_NAME));
// Actually sign the input.
return newStringUtf8(encodeBase64(mac.doFinal(
getBytesUtf8(input))));
} catch (Exception e) {
throw new AuthenticationException("Failed to SHA-256 " +
"sign input string: " + input, e);
}
}
}
}