Skip to content

Commit

Permalink
NIFI-7385 Provided reverse-indexed TokenCache implementation.
Browse files Browse the repository at this point in the history
Cleaned up code style.
Unit test was failing on Windows 1.8 GitHub Actions build but no other environment. Increased artificial delay to avoid timing issues.

Co-authored-by: Andy LoPresto <alopresto@apache.org>

This closes #4271.

Signed-off-by: Andy LoPresto <alopresto@apache.org>
  • Loading branch information
thenatog authored and alopresto committed Jun 9, 2020
1 parent aa7c5e2 commit 01e42df
Show file tree
Hide file tree
Showing 6 changed files with 604 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@
*/
package org.apache.nifi.web.security.otp;

import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.apache.nifi.web.security.NiFiAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;

import javax.servlet.http.HttpServletRequest;
import java.util.regex.Pattern;

/**
* This filter is used to capture one time passwords (OTP) from requests made to download files through the browser.
* It's required because when we initiate a download in the browser, it must be opened in a new tab. The new tab
* cannot be initialized with authentication headers, so we must add a token as a query parameter instead. As
* tokens in URL strings are visible in various places, this must only be used once - hence our OTP.
*/
public class OtpAuthenticationFilter extends NiFiAuthenticationFilter {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import org.springframework.security.core.AuthenticationException;

/**
*
* This provider will be used when the request is attempting to authenticate with a download or ui extension OTP/token.
*/
public class OtpAuthenticationProvider extends NiFiAuthenticationProvider {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,18 @@
*/
package org.apache.nifi.web.security.otp;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.apache.commons.codec.binary.Base64;
import org.apache.nifi.web.security.token.OtpAuthenticationToken;
import org.apache.nifi.web.security.util.CacheKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.nifi.web.security.token.OtpAuthenticationToken;
import org.apache.nifi.web.security.util.CacheKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* OtpService is a service for generating and verifying one time password tokens.
Expand All @@ -45,8 +41,8 @@ public class OtpService {
// protected for testing purposes
protected static final int MAX_CACHE_SOFT_LIMIT = 100;

private final Cache<CacheKey, String> downloadTokenCache;
private final Cache<CacheKey, String> uiExtensionCache;
private final TokenCache downloadTokens;
private final TokenCache uiExtensionTokens;

/**
* Creates a new OtpService with an expiration of 5 minutes.
Expand All @@ -64,8 +60,8 @@ public OtpService() {
* @throws IllegalArgumentException If duration is negative
*/
public OtpService(final int duration, final TimeUnit units) {
downloadTokenCache = CacheBuilder.newBuilder().expireAfterWrite(duration, units).build();
uiExtensionCache = CacheBuilder.newBuilder().expireAfterWrite(duration, units).build();
downloadTokens = new TokenCache("download tokens", duration, units);
uiExtensionTokens = new TokenCache("UI extension tokens", duration, units);
}

/**
Expand All @@ -75,7 +71,7 @@ public OtpService(final int duration, final TimeUnit units) {
* @return The one time use download token
*/
public String generateDownloadToken(final OtpAuthenticationToken authenticationToken) {
return generateToken(downloadTokenCache.asMap(), authenticationToken);
return generateToken(downloadTokens, authenticationToken);
}

/**
Expand All @@ -86,7 +82,7 @@ public String generateDownloadToken(final OtpAuthenticationToken authenticationT
* @throws OtpAuthenticationException When the specified token does not correspond to an authenticated identity
*/
public String getAuthenticationFromDownloadToken(final String token) throws OtpAuthenticationException {
return getAuthenticationFromToken(downloadTokenCache.asMap(), token);
return getAuthenticationFromToken(downloadTokens, token);
}

/**
Expand All @@ -96,7 +92,7 @@ public String getAuthenticationFromDownloadToken(final String token) throws OtpA
* @return The one time use UI extension token
*/
public String generateUiExtensionToken(final OtpAuthenticationToken authenticationToken) {
return generateToken(uiExtensionCache.asMap(), authenticationToken);
return generateToken(uiExtensionTokens, authenticationToken);
}

/**
Expand All @@ -107,42 +103,55 @@ public String generateUiExtensionToken(final OtpAuthenticationToken authenticati
* @throws OtpAuthenticationException When the specified token does not correspond to an authenticated identity
*/
public String getAuthenticationFromUiExtensionToken(final String token) throws OtpAuthenticationException {
return getAuthenticationFromToken(uiExtensionCache.asMap(), token);
return getAuthenticationFromToken(uiExtensionTokens, token);
}

/**
* Generates a token and stores it in the specified cache.
*
* @param cache The cache
* @param tokenCache A cache that maps tokens to users
* @param authenticationToken The authentication
* @return The one time use token
*/
private String generateToken(final ConcurrentMap<CacheKey, String> cache, final OtpAuthenticationToken authenticationToken) {
if (cache.size() >= MAX_CACHE_SOFT_LIMIT) {
throw new IllegalStateException("The maximum number of single use tokens have been issued.");
private String generateToken(final TokenCache tokenCache, final OtpAuthenticationToken authenticationToken) {
final String userId = (String) authenticationToken.getPrincipal();

// If the user has a token already, return it
if(tokenCache.containsValue(userId)) {
return (tokenCache.getKeyForValue(userId)).getKey();
} else {
// Otherwise, generate a token
if (tokenCache.size() >= MAX_CACHE_SOFT_LIMIT) {
throw new IllegalStateException("The maximum number of single use tokens have been issued.");
}

// Hash the authentication and build a cache key
final CacheKey cacheKey = new CacheKey(hash(authenticationToken));

// Store the token and user in the cache
tokenCache.put(cacheKey, userId);

// Return the token
return cacheKey.getKey();
}

// hash the authentication and build a cache key
final CacheKey cacheKey = new CacheKey(hash(authenticationToken));

// store the token unless the token is already stored which should not update it's original timestamp
cache.putIfAbsent(cacheKey, authenticationToken.getName());

// return the token
return cacheKey.getKey();
}

/**
* Gets the corresponding authentication for the specified one time use token. The specified token will be removed.
* Gets the corresponding authentication for the specified one time use token. The specified token will be removed
* from the token cache.
*
* @param cache The cache
* @param tokenCache A cache that maps tokens to users
* @param token The one time use token
* @return The authenticated identity
*/
private String getAuthenticationFromToken(final ConcurrentMap<CacheKey, String> cache, final String token) throws OtpAuthenticationException {
final String authenticatedUser = cache.remove(new CacheKey(token));
private String getAuthenticationFromToken(final TokenCache tokenCache, final String token) throws OtpAuthenticationException {
final CacheKey cacheKey = new CacheKey(token);
final String authenticatedUser = (String) tokenCache.getIfPresent(cacheKey);

if (authenticatedUser == null) {
throw new OtpAuthenticationException("Unable to validate the access token.");
} else {
tokenCache.invalidate(cacheKey);
}

return authenticatedUser;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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 org.apache.nifi.web.security.otp;

import com.google.common.cache.AbstractCache;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheStats;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import org.apache.nifi.web.security.util.CacheKey;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* This class provides a specific wrapper implementation based on the Guava {@link Cache} but with
* reverse-index capability because of the special use case (a user [the cache value] can only have
* one active token [the cache key] at a time). This allows reverse lookup semantics.
*/
public class TokenCache extends AbstractCache<CacheKey, String> {
private static final Logger logger = LoggerFactory.getLogger(TokenCache.class);

private final String contentsDescription;
private final Cache<CacheKey, String> internalCache;

public TokenCache(String contentsDescription, final int duration, final TimeUnit units) {
this.contentsDescription = contentsDescription;
internalCache = CacheBuilder.newBuilder().expireAfterWrite(duration, units).build();
}

/**
* Returns the value associated with {@code key} in this cache, or {@code null} if there is no
* cached value for {@code key}.
*
* @param key the (wrapped) {@code token}
* @since 11.0
* @return the retrieved value ({@code user})
*/
@Override
public @Nullable String getIfPresent(Object key) {
return internalCache.getIfPresent(key);
}

/**
* Puts the provided value ({@code user}) in the cache at the provided key (wrapped {@code token}).
*
* @param key the cache key
* @param value the value to insert
* @since 11.0
*/
@Override
public void put(CacheKey key, String value) {
internalCache.put(key, value);
}

/**
* Returns {@code true} if the cache contains the provided value.
*
* @param value the value ({@code user}) to look for
* @return true if the user exists in the cache
*/
public boolean containsValue(String value) {
return internalCache.asMap().containsValue(value);
}

/**
* Returns the {@link CacheKey} representing the key ({@code token}) associated with the provided value ({@code user}).
*
* @param value the value ({@code user}) to look for
* @return the CacheKey ({@code token}) associated with this user, or {@code null} if the user has no tokens in this cache
*/
@Nullable
public CacheKey getKeyForValue(String value) {
if (containsValue(value)) {
Map<CacheKey, String> cacheMap = internalCache.asMap();
for (Map.Entry<CacheKey, String> e : cacheMap.entrySet()) {
if (e.getValue().equals(value)) {
return e.getKey();
}
}
throw new IllegalStateException("The value existed in the cache but expired during retrieval");
} else {
return null;
}
}

// Override the unsupported abstract methods from the parent

@Override
public void invalidate(Object key) {
internalCache.invalidate(key);
}

@Override
public void invalidateAll() {
internalCache.invalidateAll(internalCache.asMap().keySet());
}

@Override
public long size() {
return internalCache.size();
}

@Override
public CacheStats stats() {
return internalCache.stats();
}

@Override
public ConcurrentMap<CacheKey, String> asMap() {
return internalCache.asMap();
}

/**
* Returns a string representation of the cache.
*
* @return a string representation of the cache
*/
@Override
public String toString() {
return new StringBuilder("TokenCache for ")
.append(contentsDescription)
.append(" with ")
.append(internalCache.size())
.append(" elements")
.toString();
}
}
Loading

0 comments on commit 01e42df

Please sign in to comment.