org.graylog2.security.realm.HTTPHeaderAuthenticationRealm Maven / Gradle / Ivy
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* .
*/
package org.graylog2.security.realm;
import com.google.common.base.Joiner;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAccount;
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.graylog.security.authservice.AuthServiceAuthenticator;
import org.graylog.security.authservice.AuthServiceCredentials;
import org.graylog.security.authservice.AuthServiceException;
import org.graylog.security.authservice.AuthServiceResult;
import org.graylog2.plugin.cluster.ClusterConfigService;
import org.graylog2.security.headerauth.HTTPHeaderAuthConfig;
import org.graylog2.shared.security.RemoteAddressAuthenticationToken;
import org.graylog2.shared.security.ShiroRequestHeadersBinder;
import org.graylog2.shared.security.ShiroSecurityContext;
import org.graylog2.utilities.IpSubnet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.net.UnknownHostException;
import java.util.Optional;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.isBlank;
public class HTTPHeaderAuthenticationRealm extends AuthenticatingRealm {
private static final Logger LOG = LoggerFactory.getLogger(HTTPHeaderAuthenticationRealm.class);
private static final Joiner JOINER = Joiner.on(", ");
public static final String NAME = "http-header-authentication";
public static final String SESSION_AUTH_HEADER = "http-header-auth-user";
private final ClusterConfigService clusterConfigService;
private final AuthServiceAuthenticator authServiceAuthenticator;
private final Set trustedProxies;
@Inject
public HTTPHeaderAuthenticationRealm(ClusterConfigService clusterConfigService,
AuthServiceAuthenticator authServiceAuthenticator,
@Named("trusted_proxies") Set trustedProxies) {
this.clusterConfigService = clusterConfigService;
this.authServiceAuthenticator = authServiceAuthenticator;
this.trustedProxies = trustedProxies;
setAuthenticationTokenClass(RemoteAddressAuthenticationToken.class);
setCachingEnabled(false);
// Credentials will be matched via the authentication service itself so we don't need Shiro to do it
setCredentialsMatcher(new AllowAllCredentialsMatcher());
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
final RemoteAddressAuthenticationToken remoteAddrToken = (RemoteAddressAuthenticationToken) token;
final HTTPHeaderAuthConfig config = loadConfig();
if (!config.enabled()) {
LOG.debug("Skipping disabled HTTP header authentication");
return null;
}
final Optional optionalUsername = ShiroRequestHeadersBinder.getHeaderFromThreadContext(config.usernameHeader());
if (optionalUsername.isPresent()) {
final String username = optionalUsername.get().trim();
if (isBlank(username)) {
LOG.warn("Skipping request with trusted HTTP header <{}> and blank value", config.usernameHeader());
return null;
}
final String remoteAddr = remoteAddrToken.getRemoteAddr();
if (inTrustedSubnets(remoteAddr)) {
return doAuthenticate(username, config, remoteAddr);
}
LOG.warn("Request with trusted HTTP header <{}={}> received from <{}> which is not in the trusted proxies: <{}>",
config.usernameHeader(),
username,
remoteAddr,
JOINER.join(trustedProxies));
return null;
}
return null;
}
private AuthenticationInfo doAuthenticate(String username, HTTPHeaderAuthConfig config, String remoteAddr) {
LOG.debug("Attempting authentication for username <{}>", username);
try {
// Create already authenticated credentials to make sure the auth service backend doesn't try to
// authenticate the user again
final AuthServiceCredentials credentials = AuthServiceCredentials.createAuthenticated(username);
final AuthServiceResult result = authServiceAuthenticator.authenticate(credentials);
if (result.isSuccess()) {
LOG.debug("Successfully authenticated username <{}> for user profile <{}> with backend <{}/{}/{}>",
result.username(), result.userProfileId(), result.backendTitle(), result.backendType(), result.backendId());
// Setting this, will let the SessionResource know, that when a non-existing session is validated, it
// should in fact create a session.
ShiroSecurityContext.requestSessionCreation(true);
return toAuthenticationInfo(result);
} else {
LOG.warn("Failed to authenticate username <{}> from trusted HTTP header <{}> via proxy <{}>",
result.username(), config.usernameHeader(), remoteAddr);
return null;
}
} catch (AuthServiceException e) {
LOG.error("Authentication service error", e);
return null;
} catch (Exception e) {
LOG.error("Unhandled authentication error", e);
return null;
}
}
private AuthenticationInfo toAuthenticationInfo(AuthServiceResult result) {
return new SimpleAccount(result.userProfileId(), null, NAME + "/" + result.backendType());
}
private HTTPHeaderAuthConfig loadConfig() {
return clusterConfigService.getOrDefault(HTTPHeaderAuthConfig.class, HTTPHeaderAuthConfig.createDisabled());
}
private boolean inTrustedSubnets(String remoteAddr) {
return trustedProxies.stream().anyMatch(ipSubnet -> ipSubnetContains(ipSubnet, remoteAddr));
}
private boolean ipSubnetContains(IpSubnet ipSubnet, String ipAddr) {
try {
return ipSubnet.contains(ipAddr);
} catch (UnknownHostException ignored) {
LOG.debug("Looking up remote address <{}> failed.", ipAddr);
return false;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy