blob: e8d96ae1961694afa5a91805f6231629b6b7d5c1 [file] [log] [blame]
/*
* 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 java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
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.
*/
public class OtpService {
private static final Logger logger = LoggerFactory.getLogger(OtpService.class);
private static final String HMAC_SHA256 = "HmacSHA256";
// protected for testing purposes
protected static final int MAX_CACHE_SOFT_LIMIT = 100;
private final TokenCache downloadTokens;
private final TokenCache uiExtensionTokens;
/**
* Creates a new OtpService with an expiration of 5 minutes.
*/
public OtpService() {
this(5, TimeUnit.MINUTES);
}
/**
* Creates a new OtpService.
*
* @param duration The expiration duration
* @param units The expiration units
* @throws NullPointerException If units is null
* @throws IllegalArgumentException If duration is negative
*/
public OtpService(final int duration, final TimeUnit units) {
downloadTokens = new TokenCache("download tokens", duration, units);
uiExtensionTokens = new TokenCache("UI extension tokens", duration, units);
}
/**
* Generates a download token for the specified authentication.
*
* @param authenticationToken The authentication
* @return The one time use download token
*/
public String generateDownloadToken(final OtpAuthenticationToken authenticationToken) {
return generateToken(downloadTokens, authenticationToken);
}
/**
* Gets the authenticated identity from the specified one time use download token. This method will not return null.
*
* @param token The one time use download token
* @return The authenticated identity
* @throws OtpAuthenticationException When the specified token does not correspond to an authenticated identity
*/
public String getAuthenticationFromDownloadToken(final String token) throws OtpAuthenticationException {
return getAuthenticationFromToken(downloadTokens, token);
}
/**
* Generates a UI extension token for the specified authentication.
*
* @param authenticationToken The authentication
* @return The one time use UI extension token
*/
public String generateUiExtensionToken(final OtpAuthenticationToken authenticationToken) {
return generateToken(uiExtensionTokens, authenticationToken);
}
/**
* Gets the authenticated identity from the specified one time use UI extension token. This method will not return null.
*
* @param token The one time use UI extension token
* @return The authenticated identity
* @throws OtpAuthenticationException When the specified token does not correspond to an authenticated identity
*/
public String getAuthenticationFromUiExtensionToken(final String token) throws OtpAuthenticationException {
return getAuthenticationFromToken(uiExtensionTokens, token);
}
/**
* Generates a token and stores it in the specified cache.
*
* @param tokenCache A cache that maps tokens to users
* @param authenticationToken The authentication
* @return The one time use token
*/
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();
}
}
/**
* Gets the corresponding authentication for the specified one time use token. The specified token will be removed
* from the token 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 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;
}
/**
* Hashes the specified authentication token. The resulting value will be used as the one time use token.
*
* @param authenticationToken the authentication token
* @return the one time use token
*/
private String hash(final OtpAuthenticationToken authenticationToken) {
try {
// input is the user identity and timestamp
final String input = authenticationToken.getName() + "-" + System.nanoTime();
// create the secret using secure random
final SecureRandom secureRandom = new SecureRandom();
final byte[] randomBytes = new byte[32];
secureRandom.nextBytes(randomBytes);
final SecretKeySpec secret = new SecretKeySpec(randomBytes, HMAC_SHA256); // 256 bit
// hash the input
final Mac hmacSha256 = Mac.getInstance(HMAC_SHA256);
hmacSha256.init(secret);
final byte[] output = hmacSha256.doFinal(input.getBytes(StandardCharsets.UTF_8));
// return the result as a base 64 string
return Base64.encodeBase64URLSafeString(output);
} catch (final NoSuchAlgorithmException | InvalidKeyException e) {
final String errorMessage = "There was an error generating the OTP";
logger.error(errorMessage, e);
throw new IllegalStateException("Unable to generate single use token.");
}
}
}