package jenkins.security;
import hudson.security.ACL;
import hudson.security.SecurityRealm;
import hudson.util.Scrambler;
import org.acegisecurity.Authentication;
import org.acegisecurity.BadCredentialsException;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
import org.acegisecurity.ui.AuthenticationEntryPoint;
import org.acegisecurity.ui.rememberme.NullRememberMeServices;
import org.acegisecurity.ui.rememberme.RememberMeServices;
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.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.logging.Logger;
import static java.util.logging.Level.*;
/**
* Takes "username:password" given in the <tt>Authorization</tt> HTTP header and authenticates
* the request.
*
* <p>
* Implementations of {@link BasicHeaderAuthenticator} includes one that accepts the real password,
* then one that checks the user's API token. We call them all from a single Filter like this,
* as opposed to using a list of {@link Filter}s, so that multiple filters don't end up trying
* to authenticate the same header differently and fail.
*
* @author Kohsuke Kawaguchi
* @see ZD-19640
*/
public class BasicHeaderProcessor implements Filter {
// these fields are supposed to be injected by Spring
private AuthenticationEntryPoint authenticationEntryPoint;
private RememberMeServices rememberMeServices = new NullRememberMeServices();
public void init(FilterConfig filterConfig) throws ServletException {
}
public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
this.authenticationEntryPoint = authenticationEntryPoint;
}
public void setRememberMeServices(RememberMeServices rememberMeServices) {
this.rememberMeServices = rememberMeServices;
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse rsp = (HttpServletResponse) response;
String authorization = req.getHeader("Authorization");
if (authorization!=null && authorization.startsWith("Basic ")) {
// authenticate the user
String uidpassword = Scrambler.descramble(authorization.substring(6));
int idx = uidpassword.indexOf(':');
if (idx >= 0) {
String username = uidpassword.substring(0, idx);
String password = uidpassword.substring(idx+1);
if (!authenticationIsRequired(username)) {
chain.doFilter(request, response);
return;
}
for (BasicHeaderAuthenticator a : all()) {
LOGGER.log(FINER, "Attempting to authenticate with {0}", a);
Authentication auth = a.authenticate(req, rsp, username, password);
if (auth!=null) {
LOGGER.log(FINE, "Request authenticated as {0} by {1}", new Object[]{auth,a});
success(req, rsp, chain, auth);
return;
}
}
fail(req, rsp, new BadCredentialsException("Invalid password/token for user: " + username));
} else {
fail(req, rsp, new BadCredentialsException("Malformed HTTP basic Authorization header"));
}
} else {
// not something we care
chain.doFilter(request, response);
}
}
/**
* If the request is already authenticated to the same user that the Authorization header claims,
* for example through the HTTP session, then there's no need to re-authenticate the Authorization header,
* so we skip that. This avoids stressing {@link SecurityRealm}.
*
* This method returns false if we can take this short-cut.
*/
// taken from BasicProcessingFilter.java
protected boolean authenticationIsRequired(String username) {
// Only reauthenticate if username doesn't match SecurityContextHolder and user isn't authenticated
// (see SEC-53)
Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
if(existingAuth == null || !existingAuth.isAuthenticated()) {
return true;
}
// Limit username comparison to providers which use usernames (ie UsernamePasswordAuthenticationToken)
// (see SEC-348)
if (existingAuth instanceof UsernamePasswordAuthenticationToken && !existingAuth.getName().equals(username)) {
return true;
}
// Handle unusual condition where an AnonymousAuthenticationToken is already present
// This shouldn't happen very often, as BasicProcessingFitler is meant to be earlier in the filter
// chain than AnonymousProcessingFilter. Nevertheless, presence of both an AnonymousAuthenticationToken
// together with a BASIC authentication request header should indicate reauthentication using the
// BASIC protocol is desirable. This behaviour is also consistent with that provided by form and digest,
// both of which force re-authentication if the respective header is detected (and in doing so replace
// any existing AnonymousAuthenticationToken). See SEC-610.
if (existingAuth instanceof AnonymousAuthenticationToken) {
return true;
}
return false;
}
protected void success(HttpServletRequest req, HttpServletResponse rsp, FilterChain chain, Authentication auth) throws IOException, ServletException {
rememberMeServices.loginSuccess(req, rsp, auth);
SecurityContext old = ACL.impersonate(auth);
try {
chain.doFilter(req,rsp);
} finally {
SecurityContextHolder.setContext(old);
}
}
protected void fail(HttpServletRequest req, HttpServletResponse rsp, BadCredentialsException failure) throws IOException, ServletException {
LOGGER.log(FINE, "Authentication of BASIC header failed");
rememberMeServices.loginFail(req, rsp);
authenticationEntryPoint.commence(req, rsp, failure);
}
protected List<? extends BasicHeaderAuthenticator> all() {
return BasicHeaderAuthenticator.all();
}
public void destroy() {
}
private static final Logger LOGGER = Logger.getLogger(BasicHeaderProcessor.class.getName());
}