/*
* Copyright 2014 JBoss Inc
*
* 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 io.apiman.gateway.engine.policies;
import io.apiman.gateway.engine.async.AsyncResultImpl;
import io.apiman.gateway.engine.async.IAsyncResult;
import io.apiman.gateway.engine.async.IAsyncResultHandler;
import io.apiman.gateway.engine.beans.ApiRequest;
import io.apiman.gateway.engine.beans.PolicyFailure;
import io.apiman.gateway.engine.beans.PolicyFailureType;
import io.apiman.gateway.engine.components.IPolicyFailureFactoryComponent;
import io.apiman.gateway.engine.metrics.RequestMetric;
import io.apiman.gateway.engine.policies.auth.JDBCIdentityValidator;
import io.apiman.gateway.engine.policies.auth.LDAPIdentityValidator;
import io.apiman.gateway.engine.policies.auth.StaticIdentityValidator;
import io.apiman.gateway.engine.policies.config.BasicAuthenticationConfig;
import io.apiman.gateway.engine.policies.i18n.Messages;
import io.apiman.gateway.engine.policy.IPolicyChain;
import io.apiman.gateway.engine.policy.IPolicyContext;
import io.apiman.gateway.engine.policy.PolicyContextKeys;
import org.apache.commons.codec.binary.Base64;
/**
* An implementation of an apiman policy that supports multiple styles of authentication.
* Specifically this policy is responsible for authenticating the inbound request prior
* to proxying the request to the back end API. If the authentication fails then
* the back end system is never invoked.
*
* @author eric.wittmann@redhat.com
*/
public class BasicAuthenticationPolicy extends AbstractMappedPolicy<BasicAuthenticationConfig> {
private static final StaticIdentityValidator staticIdentityValidator = new StaticIdentityValidator();
private static final LDAPIdentityValidator ldapIdentityValidator = new LDAPIdentityValidator();
private static final JDBCIdentityValidator jdbcIdentityValidator = new JDBCIdentityValidator();
/**
* Constructor.
*/
public BasicAuthenticationPolicy() {
}
/**
* @see io.apiman.gateway.engine.policy.AbstractPolicy#getConfigurationClass()
*/
@Override
protected Class<BasicAuthenticationConfig> getConfigurationClass() {
return BasicAuthenticationConfig.class;
}
/**
* @see io.apiman.gateway.engine.policies.AbstractMappedPolicy#doApply(io.apiman.gateway.engine.beans.ApiRequest, io.apiman.gateway.engine.policy.IPolicyContext, java.lang.Object, io.apiman.gateway.engine.policy.IPolicyChain)
*/
@Override
protected void doApply(final ApiRequest request, final IPolicyContext context, final BasicAuthenticationConfig config,
final IPolicyChain<ApiRequest> chain) {
// Check transport security
if (config.isRequireTransportSecurity() && !request.isTransportSecure()) {
sendAuthFailure(context, chain, config, PolicyFailureCodes.TRANSPORT_SECURITY_REQUIRED);
return;
}
String authHeader = request.getHeaders().get("Authorization"); //$NON-NLS-1$
boolean requireBasic = config.getRequireBasicAuth() == null ? Boolean.TRUE : config.getRequireBasicAuth();
// Handle the case where no authentication credentials are provided
if (authHeader == null || authHeader.trim().isEmpty()) {
if (requireBasic) {
sendAuthFailure(context, chain, config, PolicyFailureCodes.BASIC_AUTH_REQUIRED);
return;
} else {
chain.doApply(request);
return;
}
}
// Handle the case where auth credentials are provided but they aren't BASIC
// credentials (e.g. BEARER-TOKEN)
if (!authHeader.toUpperCase().startsWith("BASIC ")) { //$NON-NLS-1$
if (requireBasic) {
sendAuthFailure(context, chain, config, PolicyFailureCodes.BASIC_AUTH_REQUIRED);
return;
} else {
chain.doApply(request);
return;
}
}
// Parse the Authorization http header.
String username;
String password = null;
try {
String userpassEncoded = authHeader.substring(6);
byte[] decoded = Base64.decodeBase64(userpassEncoded);
String data = new String(decoded, "UTF-8"); //$NON-NLS-1$
int sepIdx = data.indexOf(':');
if (sepIdx > 0) {
username = data.substring(0, sepIdx);
password = data.substring(sepIdx + 1);
} else {
username = data;
}
} catch (Throwable t) {
// TODO log this error to apiman::logger
sendAuthFailure(context, chain, config, PolicyFailureCodes.BASIC_AUTH_FAILED);
return;
}
// Asynchronously validate the inbound requests's basic auth credentials
final String forwardedUsername = username;
validateCredentials(username, password, request, context, config, new IAsyncResultHandler<Boolean>() {
@Override
public void handle(IAsyncResult<Boolean> result) {
if (result.isError()) {
chain.throwError(result.getError());
} else {
if (result.getResult()) {
String forwardIdentityHttpHeader = config.getForwardIdentityHttpHeader();
if (forwardIdentityHttpHeader != null && !forwardIdentityHttpHeader.trim().isEmpty()) {
request.getHeaders().put(forwardIdentityHttpHeader, forwardedUsername);
}
RequestMetric metric = context.getAttribute(PolicyContextKeys.REQUEST_METRIC, (RequestMetric) null);
if (metric != null) {
metric.setUser(forwardedUsername);
}
// Remove the authorization header so that it doesn't get passed through to the backend API
// TODO: make this optional - perhaps they *want* the auth header passed through?
request.getHeaders().remove("Authorization"); //$NON-NLS-1$
chain.doApply(request);
} else {
sendAuthFailure(context, chain, config, PolicyFailureCodes.BASIC_AUTH_FAILED);
}
}
}
});
}
/**
* Validate the inbound authentication credentials.
* @param username
* @param password
* @param request
* @param context
* @param config
* @param handler
*/
private void validateCredentials(String username, String password, ApiRequest request, IPolicyContext context,
BasicAuthenticationConfig config, IAsyncResultHandler<Boolean> handler) {
if (config.getStaticIdentity() != null) {
staticIdentityValidator.validate(username, password, request, context, config.getStaticIdentity(), handler);
} else if (config.getLdapIdentity() != null) {
ldapIdentityValidator.validate(username, password, request, context, config.getLdapIdentity(), handler);
} else if (config.getJdbcIdentity() != null) {
jdbcIdentityValidator.validate(username, password, request, context, config.getJdbcIdentity(), handler);
} else {
handler.handle(AsyncResultImpl.create(Boolean.FALSE));
}
}
/**
* Sends the 'unauthenticated' response as a policy failure.
* @param context
* @param chain
* @param config
* @param reason
*/
protected void sendAuthFailure(IPolicyContext context, IPolicyChain<?> chain, BasicAuthenticationConfig config, int reason) {
IPolicyFailureFactoryComponent pff = context.getComponent(IPolicyFailureFactoryComponent.class);
PolicyFailure failure = pff.createFailure(PolicyFailureType.Authentication, reason, Messages.i18n.format("BasicAuthenticationPolicy.AuthenticationFailed")); //$NON-NLS-1$
String realm = config.getRealm();
if (realm == null || realm.trim().isEmpty()) {
realm = "Apiman"; //$NON-NLS-1$
}
failure.getHeaders().put("WWW-Authenticate", String.format("Basic realm=\"%1$s\"", realm)); //$NON-NLS-1$ //$NON-NLS-2$
chain.doFailure(failure);
}
}