Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Note, a bypass for http headers could exist if the user agent was auto-injecting headers when requesting certain URLs e.g. a http header modification browser plugin. 


Appendix A (Access Control Implementation)

Code Block
/*
 * Licensed to the University Corporation for Advanced Internet Development,
 * Inc. (UCAID) under one or more contributor license agreements.  See the
 * NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The UCAID licenses this file to You 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 net.shibboleth.utilities.java.support.security;

import java.util.Enumeration;
import java.util.function.Predicate;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.net.HttpHeaders;

import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
import net.shibboleth.utilities.java.support.primitive.StringSupport;

/**
 * Access control implementation based on pre-shared API keys in the HTTP Authorization header or query parameters.
 */
public class APIKeyAccessControl  extends AbstractIdentifiableInitializableComponent implements AccessControl{
    
    
   /** Class logger. */
    @Nonnull private final Logger log = LoggerFactory.getLogger(APIKeyAccessControl.class);
    
    /** The API key to match in the request. */
    @NonnullAfterInit @NotEmpty private String apiKey;
    
    /** The HTTP Authorization scheme name.*/
    @Nonnull @NotEmpty public static final String API_KEY_HEADER_SCHEME="SHIB-API-KEY";
    
    /** The API key query parameter name.  */
    @Nonnull @NotEmpty public static final String API_KEY_PARAM_NAME="shibapikey";
    
    /** Whether API key has sufficient entropy, or conforms to a specific policy? */
    @Nonnull private Predicate<String> apiKeyValidationPolicyPredicate;
    
    /** Where the API key can be found in the request.   */
    public enum APIKeyIn{
        /** The API Key is contained in the HTTP Authorization header. */
        HEADER,
        /** The API Key is contained in a query string parameter. */
        QUERY,
    }
    
    /** Where the API key can be found in the request. Default is HEADER.*/
    @Nonnull private APIKeyIn apiKeyIn;
    
    /**
     * 
     * Constructor.
     *
     */
    public APIKeyAccessControl() {      
        apiKeyIn = APIKeyIn.HEADER;
        apiKeyValidationPolicyPredicate = new LengthBasedAPIKeyPolicyValidator();
    }
    
    @Override protected void doInitialize() throws ComponentInitializationException { 

        if (apiKey == null) {
            throw new ComponentInitializationException("API Key cannot be null");
        }
        
       if (!apiKeyValidationPolicyPredicate.test(apiKey)) {           
           throw new ComponentInitializationException("API Key does not satisfy validation policy");
       }
    
    }
    
    /**
     * Set the API key validation policy.
     * 
     * @param keyPolicy the API key validation policy.
     */
    public void setApiKeyValidationPolicyPredicate(@Nonnull final Predicate<String> keyPolicy) {
        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
        apiKeyValidationPolicyPredicate = Constraint.isNotNull(keyPolicy, "API key validation policy must not be null");
    }
    
    /**
     * Set the API Key.
     * 
     * @param key the API key, can not be {@literal null} or {@literal empty}.
     */
    public void setApiKey(@Nonnull @NotEmpty final String key) {
        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);             
        apiKey = Constraint.isNotEmpty(key, "API Key cannot be null or empty");
    }
    
    /**
     * Sets where to find the API key, either the HTTP header or the query parameters.
     * 
     * @param keyIn where to find the API key.
     */
    public void setApiKeyIn(@Nonnull final APIKeyIn keyIn) {
        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
        apiKeyIn = Constraint.isNotNull(keyIn, "API Key In (Header or Query Parameters) can not be null");
    }

    /** {@inheritDoc} */
    @Nonnull public boolean checkAccess(@Nonnull final ServletRequest request, @Nullable final String operation,
            @Nullable final String resource) {
      
       final String addr = request.getRemoteAddr()!=null ? request.getRemoteAddr():"unknown";

       if (request instanceof HttpServletRequest) {
           final HttpServletRequest httpRequest = (HttpServletRequest) request;  
           
          String requestApiKey = null;
           
           if (APIKeyIn.HEADER == apiKeyIn) {       
       
               requestApiKey = extractApiKeyFromHeaders(httpRequest);  
               
           } else if (APIKeyIn.QUERY == apiKeyIn) {              
               
               requestApiKey = extractApiKeyFromParameters(httpRequest);             
           }             
           if (apiKey.equals(requestApiKey)) {
               log.debug("{} Granted access to client [{}] using matching API key (Operation: {}, Resource: {})",
                       new Object[] {getLogPrefix(), addr, operation, resource});
               return true;
           }else {
               log.debug("{} Denied request from client [{}], API keys do not match (Operation: {}, Resource: {})",
                       new Object[] {getLogPrefix(), addr, operation, resource});
               return false;
           }
           
       } else {
           log.warn("{} Denied request from client [{}], request is not a HTTP request (Operation: {}, Resource: {})",
                  getLogPrefix(),addr, operation, resource);
           return false;
       }
       
      
       
    }
    
    /**
     * Gets the API key passed in via the HTTP parameters. Only one API key value is expected.
     *  
     * @param httpRequest current HTTP request
     * @return the API key, or {@literal null}
     */
    @Nullable private String extractApiKeyFromParameters(@Nonnull final HttpServletRequest httpRequest) {
        
        final String[] apiKeyValues = httpRequest.getParameterValues(API_KEY_PARAM_NAME);
      
        if (apiKeyValues==null || apiKeyValues.length!=1) {
            log.warn("{} One API key parameter expected, has {}",
                    getLogPrefix(),apiKeyValues==null?"none":apiKeyValues.length);
            return null;
        }
        
        return StringSupport.trimOrNull(apiKeyValues[0]);
       
    }
    
    /**
     * Gets the API key passed in via the {@link HttpHeaders#AUTHORIZATION} header. This method checks to
     * ensure that the authentication scheme is {@link #API_KEY_HEADER_SCHEME} and then strips off
     * and returns the follow on key. Extracts the first of such found.
     * 
     * @param httpRequest current HTTP request
     * 
     * @return the API key, or {@literal null}
     */
    @Nullable private String extractApiKeyFromHeaders(@Nonnull final HttpServletRequest httpRequest) {
        
        final Enumeration<String> header = httpRequest.getHeaders(HttpHeaders.AUTHORIZATION);
        
        while (header.hasMoreElements()) {
            final String[] splitValue = header.nextElement().split(" ");
            if (splitValue.length == 2) {
                final String authnScheme = StringSupport.trimOrNull(splitValue[0]);
                if (API_KEY_HEADER_SCHEME.equalsIgnoreCase(authnScheme)) {
                    return StringSupport.trimOrNull(splitValue[1]);
                }
            }
        }
        
        
        log.debug("{} No appropriate Authorization header found", getLogPrefix());
        return null;
    }
    
    
    /**
     * Get logging prefix.
     * 
     * @return  prefix
     */
    @Nonnull private String getLogPrefix() {
        return "Policy " + getId() + ":";
    }
    
    /**
     * Simple, default, API key validator. Requires the key to be >= 9 characters.
     */
    class LengthBasedAPIKeyPolicyValidator implements Predicate<String>{

        /** {@inheritDoc} */
        @Nonnull public boolean test(@Nullable final String key) {
            if (StringSupport.trimOrNull(key)==null) {
                return false;
            }
            if (key.length()>=9) {
                return true;
            }
            return false;
        }
        
    }

}