/*
* Copyright 2015 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.plugins.keycloak_oauth_policy;
import java.util.Collections;
import org.apache.commons.lang.StringUtils;
import org.keycloak.RSATokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessToken.Access;
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.components.ISharedStateComponent;
import io.apiman.gateway.engine.metrics.RequestMetric;
import io.apiman.gateway.engine.policies.AbstractMappedPolicy;
import io.apiman.gateway.engine.policies.AuthorizationPolicy;
import io.apiman.gateway.engine.policy.IPolicyChain;
import io.apiman.gateway.engine.policy.IPolicyContext;
import io.apiman.gateway.engine.policy.PolicyContextKeys;
import io.apiman.plugins.keycloak_oauth_policy.beans.ForwardAuthInfo;
import io.apiman.plugins.keycloak_oauth_policy.beans.KeycloakOauthConfigBean;
import io.apiman.plugins.keycloak_oauth_policy.failures.PolicyFailureFactory;
import io.apiman.plugins.keycloak_oauth_policy.util.Holder;
/**
* A Keycloak OAuth policy.
*
* @author Marc Savy {@literal <msavy@redhat.com>}
*/
public class KeycloakOauthPolicy extends AbstractMappedPolicy<KeycloakOauthConfigBean> {
private static final String AUTHORIZATION_KEY = "Authorization"; //$NON-NLS-1$
private static final String ACCESS_TOKEN_QUERY_KEY = "access_token"; //$NON-NLS-1$
private static final String BEARER = "Bearer "; //$NON-NLS-1$
private static final String NEGOTIATE = "Negotiate "; //$NON-NLS-1$
private final PolicyFailureFactory failureFactory = new PolicyFailureFactory();
/**
* @see io.apiman.gateway.engine.policies.AbstractMappedPolicy#getConfigurationClass()
*/
@Override
protected Class<KeycloakOauthConfigBean> getConfigurationClass() {
return KeycloakOauthConfigBean.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 KeycloakOauthConfigBean config, final IPolicyChain<ApiRequest> chain) {
final String rawToken = getRawAuthToken(request);
final Holder<Boolean> successStatus = new Holder<>(true);
if (rawToken == null) {
if (config.getRequireOauth()) {
doFailure(successStatus, chain, failureFactory.noAuthenticationProvided(context));
} else {
chain.doApply(request);
}
} else if (doTokenAuth(successStatus, request, context, config, chain, rawToken).getValue()) {
// Transport security check
if (config.getRequireTransportSecurity() && !request.isTransportSecure()) {
// If we've detected a situation where we should blacklist a
// token
if (config.getBlacklistUnsafeTokens()) {
blacklistToken(context, rawToken, new IAsyncResultHandler<Void>() {
@Override
public void handle(IAsyncResult<Void> result) {
if (result.isError()) {
// TODO log the error (need a policy logger)
}
}
});
}
doFailure(successStatus, chain, failureFactory.noTransportSecurity(context));
return;
}
// If enabled we check against the blacklist
if (config.getBlacklistUnsafeTokens()) {
isBlacklistedToken(context, rawToken, new IAsyncResultHandler<Boolean>() {
@Override
public void handle(IAsyncResult<Boolean> result) {
if (result.isError()) {
throwError(successStatus, chain, result.getError());
} else if (result.getResult()) {
doFailure(successStatus, chain, failureFactory.blacklistedToken(context));
} else {
chain.doApply(request);
}
}
});
} else {
if (successStatus.getValue()) {
chain.doApply(request);
}
}
}
}
private void doFailure(Holder<Boolean> successStatus, IPolicyChain<?> chain, PolicyFailure failure) {
chain.doFailure(failure);
successStatus.setValue(false);
}
private void throwError(Holder<Boolean> successStatus, IPolicyChain<?> chain, Throwable error) {
chain.throwError(error);
successStatus.setValue(false);
}
private Holder<Boolean> doTokenAuth(Holder<Boolean> successStatus, ApiRequest request,
IPolicyContext context, KeycloakOauthConfigBean config, IPolicyChain<ApiRequest> chain,
String rawToken) {
try {
AccessToken parsedToken = RSATokenVerifier.verifyToken(rawToken, config.getRealmCertificate()
.getPublicKey(), config.getRealm());
delegateKerberosTicket(request, config, parsedToken);
forwardHeaders(request, config, rawToken, parsedToken);
stripAuthTokens(request, config);
forwardAuthRoles(context, config, parsedToken);
RequestMetric metric = context.getAttribute(PolicyContextKeys.REQUEST_METRIC, (RequestMetric) null);
if (metric != null) {
metric.setUser(parsedToken.getPreferredUsername());
}
return successStatus.setValue(true);
} catch (VerificationException e) {
System.out.println(e);
chain.doFailure(failureFactory.verificationException(context, e));
return successStatus.setValue(false);
}
}
private void forwardAuthRoles(IPolicyContext context, KeycloakOauthConfigBean config,
AccessToken parsedToken) {
if (config.getForwardRoles().getActive()) {
Access access = null;
if (config.getForwardRoles().getApplicationName() != null) {
access = parsedToken.getResourceAccess(config.getForwardRoles().getApplicationName());
} else {
access = parsedToken.getRealmAccess();
}
if (access == null || access.getRoles() == null) {
context.setAttribute(AuthorizationPolicy.AUTHENTICATED_USER_ROLES, Collections.<String>emptySet());
} else {
context.setAttribute(AuthorizationPolicy.AUTHENTICATED_USER_ROLES, access.getRoles());
}
}
}
private void delegateKerberosTicket(ApiRequest request, KeycloakOauthConfigBean config,
AccessToken parsedToken) {
String serializedGssCredential = (String) parsedToken.getOtherClaims().get(
KerberosConstants.GSS_DELEGATION_CREDENTIAL);
if (config.getDelegateKerberosTicket()) {
request.getHeaders().put(AUTHORIZATION_KEY, NEGOTIATE + serializedGssCredential);
}
}
private String getRawAuthToken(ApiRequest request) {
String rawToken = StringUtils.strip(request.getHeaders().get(AUTHORIZATION_KEY));
if (rawToken != null && StringUtils.startsWithIgnoreCase(rawToken, BEARER)) {
rawToken = StringUtils.removeStartIgnoreCase(rawToken, BEARER);
} else {
rawToken = request.getQueryParams().get(ACCESS_TOKEN_QUERY_KEY);
}
return rawToken;
}
private void stripAuthTokens(ApiRequest request, KeycloakOauthConfigBean config) {
if (config.getStripTokens()) {
request.getHeaders().remove(AUTHORIZATION_KEY);
request.getQueryParams().remove(ACCESS_TOKEN_QUERY_KEY);
}
}
private void forwardHeaders(ApiRequest request, KeycloakOauthConfigBean config, String rawToken,
AccessToken parsedToken) {
for (ForwardAuthInfo entry : config.getForwardAuthInfo()) {
String headerValue = isToken(entry.getField()) ? rawToken :
ClaimLookup.getClaim(parsedToken, entry.getField());
// Add the header if we've been able to look it up, else it'll just be empty.
request.getHeaders().put(entry.getHeader(), headerValue);
}
}
@SuppressWarnings("nls")
private boolean isToken(String field) {
return field.toLowerCase().equals("access_token");
}
private void isBlacklistedToken(IPolicyContext context, String rawToken,
final IAsyncResultHandler<Boolean> resultHandler) {
ISharedStateComponent dataStore = getDataStore(context);
dataStore.<Boolean> getProperty("apiman-keycloak-blacklist", rawToken, false, //$NON-NLS-1$
resultHandler);
}
private void blacklistToken(IPolicyContext context, String rawToken,
final IAsyncResultHandler<Void> resultHandler) {
ISharedStateComponent dataStore = getDataStore(context);
dataStore.<Boolean> setProperty("apiman-keycloak-blacklist", rawToken, true, //$NON-NLS-1$
resultHandler);
}
private ISharedStateComponent getDataStore(IPolicyContext context) {
return context.getComponent(ISharedStateComponent.class);
}
}