package com.bazaarvoice.auth.hmac.server; import com.bazaarvoice.auth.hmac.common.Credentials; import com.bazaarvoice.auth.hmac.common.SignatureGenerator; import com.bazaarvoice.auth.hmac.common.TimeUtils; import org.joda.time.DateTime; import org.joda.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.security.MessageDigest; import java.util.concurrent.TimeUnit; import static com.bazaarvoice.auth.hmac.common.TimeUtils.nowInUTC; /** * AbstractAuthenticator is an abstract implementation of {@link Authenticator} that validates a set of * request credentials and returns the principal that the credentials identify. This class provides common * validation features, such as ensuring that the request has a valid timestamp and signature. * * @param <Principal> the type of principal the authenticator returns */ public abstract class AbstractAuthenticator<Principal> implements Authenticator<Principal> { private static final Logger LOG = LoggerFactory.getLogger(AbstractAuthenticator.class); private final long allowedTimestampRange; // in milliseconds /** * Constructs an instance using a default timestamp range of 15 minutes. This is the length of time * for which the timestamp on a request can differ from the time on the server when the server receives * the request. If the difference exceeds this range, then the request will be denied. */ protected AbstractAuthenticator() { this(15, TimeUnit.MINUTES); } /** * Constructs an instance using the specified timestamp range. This is the length of time for which * the timestamp on a request can differ from the time on the server when the server receives the * request. If the difference exceeds this range, then the request will be denied. * * @param allowedTimestampSlop the length of time for which the timestamp on a request can differ * from the server time when the request is received * @param timeUnit the unit {@code allowedTimestampSlop} is expressed in */ protected AbstractAuthenticator(long allowedTimestampSlop, TimeUnit timeUnit) { this.allowedTimestampRange = timeUnit.toMillis(allowedTimestampSlop); } @Override public Principal authenticate(Credentials credentials) { // Make sure the timestamp has not expired - this is to protect against replay attacks if (!validateTimestamp(credentials.getTimestamp())) { LOG.info("Invalid timestamp"); return null; } // Get the principal identified by the credentials Principal principal = getPrincipal(credentials); if (principal == null) { LOG.info("Could not get principal"); return null; } // Get the secret key and use it to validate the request signature String secretKey = getSecretKeyFromPrincipal(principal); if (!validateSignature(credentials, secretKey)) { LOG.info("Invalid signature"); return null; } return principal; } /** * Retrieve the principal object identified by the request credentials. * * @param credentials the credentials specified on the request * @return the principal object */ protected abstract Principal getPrincipal(Credentials credentials); /** * Retrieve the secret key for the given principal. * * @param principal the principal for which to retrieve the secret key * @return the secret key */ protected abstract String getSecretKeyFromPrincipal(Principal principal); /** * To protect against replay attacks, make sure the timestamp on the request is valid * by ensuring that the difference between the request time and the current time on the * server does not fall outside the acceptable time range. Note that the request time * may have been generated on a different machine and so it may be ahead or behind the * current server time. * * @param timestamp the timestamp specified on the request (in standard ISO8601 format) * @return true if the timestamp is valid */ private boolean validateTimestamp(String timestamp) { DateTime requestTime = TimeUtils.parse(timestamp); long difference = Math.abs(new Duration(requestTime, nowInUTC()).getMillis()); return difference <= allowedTimestampRange; } /** * Validate the signature on the request by generating a new signature here and making sure * they match. The only way for them to match is if both signature are generated using the * same secret key. If they match, this means that the requester has a valid secret key and * can be a trusted source. * * @param credentials the credentials specified on the request * @param secretKey the secret key that will be used to generate the signature * @return true if the signature is valid */ private boolean validateSignature(Credentials credentials, String secretKey) { String clientSignature = credentials.getSignature(); String serverSignature = createSignature(credentials, secretKey); return MessageDigest.isEqual(clientSignature.getBytes(), serverSignature.getBytes()); } /** * Create a signature given the set of request credentials and a secret key. * * @param credentials the credentials specified on the request * @param secretKey the secret key that will be used to generate the signature * @return the signature */ private String createSignature(Credentials credentials, String secretKey) { return new SignatureGenerator().generate( secretKey, credentials.getMethod(), credentials.getTimestamp(), credentials.getPath(), credentials.getContent()); } }