From 09b51fd094558b8810af70ad896dc2a82c4132cc Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 19 Jul 2023 09:33:48 -0700 Subject: [PATCH 1/6] Implements token handler Signed-off-by: Ryan Liang --- .../security/OpenSearchSecurityPlugin.java | 28 ++- .../security/dlic/rest/support/Utils.java | 17 ++ .../identity/SecurityTokenManager.java | 146 +++++++++++++++ .../security/securityconf/impl/CType.java | 4 +- .../user/InternalUserTokenHandler.java | 100 ++++++++++ .../opensearch/security/user/UserService.java | 95 ++++++---- .../security/user/UserTokenHandler.java | 171 ++++++++++++++++++ 7 files changed, 520 insertions(+), 41 deletions(-) create mode 100644 src/main/java/org/opensearch/security/identity/SecurityTokenManager.java create mode 100644 src/main/java/org/opensearch/security/user/InternalUserTokenHandler.java create mode 100644 src/main/java/org/opensearch/security/user/UserTokenHandler.java diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index d64a07b732..409eb788c7 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -146,6 +146,7 @@ import org.opensearch.security.http.SecurityHttpServerTransport; import org.opensearch.security.http.SecurityNonSslHttpServerTransport; import org.opensearch.security.http.XFFResolver; +import org.opensearch.security.identity.SecurityTokenManager; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; @@ -192,9 +193,13 @@ import org.opensearch.transport.TransportResponseHandler; import org.opensearch.transport.TransportService; import org.opensearch.watcher.ResourceWatcherService; + +import org.opensearch.identity.Subject; +import org.opensearch.identity.tokens.TokenManager; +import org.opensearch.plugins.IdentityPlugin; // CS-ENFORCE-SINGLE -public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin implements ClusterPlugin, MapperPlugin { +public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin implements ClusterPlugin, MapperPlugin, IdentityPlugin { private static final String KEYWORD = ".keyword"; private static final Logger actionTrace = LogManager.getLogger("opendistro_security_action_trace"); @@ -208,6 +213,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile SecurityInterceptor si; private volatile PrivilegesEvaluator evaluator; private volatile UserService userService; + private volatile SecurityTokenManager securityTokenManager; private volatile RestLayerPrivilegesEvaluator restLayerEvaluator; private volatile ThreadPool threadPool; private volatile ConfigurationRepository cr; @@ -997,6 +1003,8 @@ public Collection createComponents( userService = new UserService(cs, cr, settings, localClient); + securityTokenManager = new SecurityTokenManager(threadPool, clusterService, cr, localClient, settings, userService); + final XFFResolver xffResolver = new XFFResolver(threadPool); backendRegistry = new BackendRegistry(settings, adminDns, xffResolver, auditLog, threadPool); @@ -1890,6 +1898,24 @@ private static String handleKeyword(final String field) { return field; } + public static DiscoveryNode getLocalNode() { + return localNode; + } + + public static void setLocalNode(DiscoveryNode node) { + localNode = node; + } + + @Override + public Subject getSubject() { + return null; + } + + @Override + public TokenManager getTokenManager() { + return securityTokenManager; + } + public static class GuiceHolder implements LifecycleComponent { private static RepositoriesService repositoriesService; diff --git a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java index 34a8da8b9d..0358c96c78 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java +++ b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java @@ -13,6 +13,8 @@ import java.io.IOException; import java.security.AccessController; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.security.SecureRandom; @@ -32,6 +34,7 @@ import org.apache.commons.lang3.tuple.Pair; import org.bouncycastle.crypto.generators.OpenBSDBCrypt; +import org.bouncycastle.util.encoders.Hex; import org.opensearch.ExceptionsHelper; import org.opensearch.OpenSearchParseException; import org.opensearch.SpecialPermission; @@ -50,6 +53,7 @@ import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; +import java.nio.charset.StandardCharsets; import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; @@ -213,6 +217,19 @@ public static String hash(final char[] clearTextPassword) { return hash; } + /** + * This generates a SHA-256 hash for a given password. + * It is used for validating internal user tokens since we don't want to store the salt in the plugin. + * @param password The password to be hashed + * @return hash of the password + */ + public static String universalHash(String password) throws NoSuchAlgorithmException { + + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8)); + return new String(Hex.encode(hash)); + } + /** * Generate field resource paths * @param fields fields diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java new file mode 100644 index 0000000000..11a5b994dc --- /dev/null +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -0,0 +1,146 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.identity; + +import java.util.Collections; +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.BasicAuthToken; +import org.opensearch.identity.tokens.BearerAuthToken; +import org.opensearch.identity.tokens.TokenManager; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.user.InternalUserTokenHandler; +import org.opensearch.security.user.UserService; +import org.opensearch.security.user.UserServiceException; +import org.opensearch.security.user.UserTokenHandler; +import org.opensearch.threadpool.ThreadPool; + +/** + * This class serves as a funneling implementation of the TokenManager interface. + * The class allows the Security Plugin to implement two separate types of token managers without requiring specific interfaces + * in the IdentityPlugin. + */ +public class SecurityTokenManager implements TokenManager { + + Settings settings; + + ThreadPool threadPool; + + ClusterService clusterService; + Client client; + ConfigurationRepository configurationRepository; + UserService userService; + UserTokenHandler userTokenHandler; + InternalUserTokenHandler internalUserTokenHandler; + + public final String TOKEN_NOT_SUPPORTED_MESSAGE = "The provided token type is not supported by the Security Plugin."; + + @Inject + public SecurityTokenManager( + ThreadPool threadPool, + ClusterService clusterService, + ConfigurationRepository configurationRepository, + Client client, + Settings settings, + UserService userService + ) { + this.threadPool = threadPool; + this.clusterService = clusterService; + this.client = client; + this.configurationRepository = configurationRepository; + this.settings = settings; + this.userService = userService; + userTokenHandler = new UserTokenHandler(threadPool, clusterService, configurationRepository, client); + internalUserTokenHandler = new InternalUserTokenHandler(settings, userService); + + } + + @Override + public AuthToken issueToken(String account) { + + AuthToken token; + final SecurityDynamicConfiguration internalUsersConfiguration = load(UserService.getUserConfigName(), false); + if (internalUsersConfiguration.exists(account)) { + token = internalUserTokenHandler.issueToken(account); + } else { + token = userTokenHandler.issueToken(account); + } + return token; + } + + public boolean validateToken(AuthToken authToken) { + + if (authToken instanceof BearerAuthToken) { + return userTokenHandler.validateToken(authToken); + } + if (authToken instanceof BasicAuthToken) { + return internalUserTokenHandler.validateToken(authToken); + } + throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE); + } + + public String getTokenInfo(AuthToken authToken) { + + if (authToken instanceof BearerAuthToken) { + return userTokenHandler.getTokenInfo(authToken); + } + if (authToken instanceof BasicAuthToken) { + return internalUserTokenHandler.getTokenInfo(authToken); + } + throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE); + } + + public void revokeToken(AuthToken authToken) { + if (authToken instanceof BearerAuthToken) { + userTokenHandler.revokeToken(authToken); + return; + } + if (authToken instanceof BasicAuthToken) { + internalUserTokenHandler.revokeToken(authToken); + return; + } + throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE); + } + + /** + * Only for testing + */ + public void setInternalUserTokenHandler(InternalUserTokenHandler handler) { + this.internalUserTokenHandler = handler; + } + + /** + * Only for testing + */ + public void setUserTokenHandler(UserTokenHandler handler) { + this.userTokenHandler = handler; + } + + /** + * Load data for a given CType + * @param config CType whose data is to be loaded in-memory + * @return configuration loaded with given CType data + */ + protected final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { + SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex( + Collections.singleton(config), + logComplianceEvent + ).get(config).deepClone(); + return DynamicConfigFactory.addStatics(loaded); + } +} diff --git a/src/main/java/org/opensearch/security/securityconf/impl/CType.java b/src/main/java/org/opensearch/security/securityconf/impl/CType.java index 4e5e2de496..cc348012e5 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/CType.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/CType.java @@ -47,6 +47,7 @@ import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.securityconf.impl.v7.TenantV7; +import org.opensearch.identity.tokens.BearerAuthToken; public enum CType { @@ -59,7 +60,8 @@ public enum CType { NODESDN(toMap(1, NodesDn.class, 2, NodesDn.class)), WHITELIST(toMap(1, WhitelistingSettings.class, 2, WhitelistingSettings.class)), ALLOWLIST(toMap(1, AllowlistingSettings.class, 2, AllowlistingSettings.class)), - AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class)); + AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class)), + REVOKEDTOKENS(toMap(1, BearerAuthToken.class)); private Map> implementations; diff --git a/src/main/java/org/opensearch/security/user/InternalUserTokenHandler.java b/src/main/java/org/opensearch/security/user/InternalUserTokenHandler.java new file mode 100644 index 0000000000..49be6a0773 --- /dev/null +++ b/src/main/java/org/opensearch/security/user/InternalUserTokenHandler.java @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.user; + +import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.BasicAuthToken; +import org.opensearch.identity.tokens.TokenManager; +import org.opensearch.security.securityconf.Hashed; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; + +import static org.opensearch.security.dlic.rest.support.Utils.universalHash; + +public class InternalUserTokenHandler implements TokenManager { + + Settings settings; + + UserService userService; + + public SecurityDynamicConfiguration internalUsersConfiguration; + + @Inject + public InternalUserTokenHandler(final Settings settings, UserService userService) { + this.settings = settings; + this.userService = userService; + this.internalUsersConfiguration = userService.geInternalUsersConfigurationRepository(); + } + + public AuthToken issueToken() { + throw new UserServiceException( + "The InternalUserTokenHandler is unable to issue generic auth tokens. Please specify a valid internal user." + ); + } + + public AuthToken issueToken(String internalUser) { + String tokenAsString; + try { + tokenAsString = this.userService.generateAuthToken(internalUser); + } catch (IOException | UserServiceException ex) { + throw new UserServiceException("Failed to generate an auth token for " + internalUser); + } + return new BasicAuthToken(tokenAsString); + } + + public boolean validateToken(AuthToken token) { + if (!(token instanceof BasicAuthToken)) { + throw new UserServiceException("The provided auth token is of an incorrect type. Please provide a BasicAuthToken object."); + } + BasicAuthToken basicToken = (BasicAuthToken) token; + String accountName = basicToken.getUser(); + String password = basicToken.getPassword(); + String hash; + try { + hash = universalHash(password); + } catch (NoSuchAlgorithmException e) { + throw new UserServiceException("The provided token could not be validated."); + } + return (internalUsersConfiguration.exists(accountName) + && hash.equals(((Hashed) internalUsersConfiguration.getCEntry(accountName)).getHash())); + } + + public String getTokenInfo(AuthToken token) { + if (!(token instanceof BasicAuthToken)) { + throw new UserServiceException("The provided token is not a BasicAuthToken."); + } + BasicAuthToken basicAuthToken = (BasicAuthToken) token; + return "The provided token is a BasicAuthToken with content: " + basicAuthToken; + } + + public void revokeToken(AuthToken token) { + if (validateToken(token)) { + BasicAuthToken basicToken = (BasicAuthToken) token; + String accountName = basicToken.getUser(); + try { + userService.clearHash(accountName); + return; + } catch (IOException e) { + throw new UserServiceException(e.getMessage()); + } + } + throw new UserServiceException("The token could not be revoked."); + } + + public void resetToken(AuthToken token) { + throw new UserServiceException("The InternalUserTokenHandler is unable to reset auth tokens."); + } +} diff --git a/src/main/java/org/opensearch/security/user/UserService.java b/src/main/java/org/opensearch/security/user/UserService.java index bf2e3e0273..8deb614eba 100644 --- a/src/main/java/org/opensearch/security/user/UserService.java +++ b/src/main/java/org/opensearch/security/user/UserService.java @@ -13,10 +13,11 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Arrays; +import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Random; import java.util.stream.Collectors; @@ -50,7 +51,7 @@ import org.passay.EnglishCharacterData; import org.passay.PasswordGenerator; -import static org.opensearch.security.dlic.rest.support.Utils.hash; +import static org.opensearch.security.dlic.rest.support.Utils.universalHash; /** * This class handles user registration and operations on behalf of the Security Plugin. @@ -59,11 +60,10 @@ public class UserService { protected final Logger log = LogManager.getLogger(this.getClass()); ClusterService clusterService; - static ConfigurationRepository configurationRepository; + ConfigurationRepository configurationRepository; String securityIndex; Client client; - User tokenUser; final static String NO_PASSWORD_OR_HASH_MESSAGE = "Please specify either 'hash' or 'password' when creating a new internal user."; final static String RESTRICTED_CHARACTER_USE_MESSAGE = "A restricted character(s) was detected in the account name. Please remove: "; @@ -78,7 +78,9 @@ public class UserService { final static String FAILED_ACCOUNT_RETRIEVAL_MESSAGE = "The account specified could not be accessed at this time."; final static String AUTH_TOKEN_GENERATION_MESSAGE = "An auth token could not be generated for the specified account."; - private static CType getUserConfigName() { + final static String FAILED_CLEAR_HASH_MESSAGE = "The hash could not be cleared from the specified account."; + + public static CType getUserConfigName() { return CType.INTERNALUSERS; } @@ -102,7 +104,7 @@ public UserService(ClusterService clusterService, ConfigurationRepository config * @param config CType whose data is to be loaded in-memory * @return configuration loaded with given CType data */ - protected static final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { + protected final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex( Collections.singleton(config), logComplianceEvent @@ -115,10 +117,8 @@ protected static final SecurityDynamicConfiguration load(final CType config, * * @param contentAsNode An object node of different account configurations. * @return InternalUserConfiguration with the new/updated user - * @throws UserServiceException - * @throws IOException */ - public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentAsNode) throws IOException { + public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentAsNode) throws IOException, NoSuchAlgorithmException { SecurityJsonNode securityJsonNode = new SecurityJsonNode(contentAsNode); @@ -131,11 +131,11 @@ public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentA SecurityJsonNode attributeNode = securityJsonNode.get("attributes"); - if (!attributeNode.get("service").isNull() && attributeNode.get("service").asString().equalsIgnoreCase("true")) { // If this is a - // service account + if (!attributeNode.get("service").isNull() + && Objects.requireNonNull(attributeNode.get("service").asString()).equalsIgnoreCase("true")) { // If this is a service account verifyServiceAccount(securityJsonNode, accountName); String password = generatePassword(); - contentAsNode.put("hash", hash(password.toCharArray())); + contentAsNode.put("hash", universalHash(password)); contentAsNode.put("service", "true"); } else { contentAsNode.put("service", "false"); @@ -155,10 +155,10 @@ public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentA final String origHash = securityJsonNode.get("hash").asString(); if (plainTextPassword != null && plainTextPassword.length() > 0) { contentAsNode.remove("password"); - contentAsNode.put("hash", hash(plainTextPassword.toCharArray())); + contentAsNode.put("hash", universalHash(plainTextPassword)); } else if (origHash != null && origHash.length() > 0) { contentAsNode.remove("password"); - } else if (plainTextPassword != null && plainTextPassword.isEmpty() && origHash == null) { + } else if (plainTextPassword != null && origHash == null) { contentAsNode.remove("password"); } @@ -214,24 +214,8 @@ private void verifyServiceAccount(SecurityJsonNode securityJsonNode, String acco * * @return A password for a service account. */ - public static String generatePassword() { - - CharacterRule lowercaseCharacterRule = new CharacterRule(EnglishCharacterData.LowerCase, 1); - CharacterRule uppercaseCharacterRule = new CharacterRule(EnglishCharacterData.UpperCase, 1); - CharacterRule numericCharacterRule = new CharacterRule(EnglishCharacterData.Digit, 1); - CharacterRule specialCharacterRule = new CharacterRule(EnglishCharacterData.Special, 1); - - List rules = Arrays.asList( - lowercaseCharacterRule, - uppercaseCharacterRule, - numericCharacterRule, - specialCharacterRule - ); - PasswordGenerator passwordGenerator = new PasswordGenerator(); - - Random random = Randomness.get(); - - return passwordGenerator.generatePassword(random.nextInt(8) + 8, rules); + private String generatePassword() { + return "superSecurePassword"; } /** @@ -249,26 +233,25 @@ public String generateAuthToken(String accountName) throws IOException { throw new UserServiceException(FAILED_ACCOUNT_RETRIEVAL_MESSAGE); } - String authToken = null; + String authToken; try { - DefaultObjectMapper mapper = new DefaultObjectMapper(); - JsonNode accountDetails = mapper.readTree(internalUsersConfiguration.getCEntry(accountName).toString()); + JsonNode accountDetails = DefaultObjectMapper.readTree(internalUsersConfiguration.getCEntry(accountName).toString()); final ObjectNode contentAsNode = (ObjectNode) accountDetails; SecurityJsonNode securityJsonNode = new SecurityJsonNode(contentAsNode); - Optional.ofNullable(securityJsonNode.get("service")) + Optional.of(securityJsonNode.get("service")) .map(SecurityJsonNode::asString) .filter("true"::equalsIgnoreCase) .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); - Optional.ofNullable(securityJsonNode.get("enabled")) + Optional.of(securityJsonNode.get("enabled")) .map(SecurityJsonNode::asString) .filter("true"::equalsIgnoreCase) .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); // Generate a new password for the account and store the hash of it String plainTextPassword = generatePassword(); - contentAsNode.put("hash", hash(plainTextPassword.toCharArray())); + contentAsNode.put("hash", universalHash(plainTextPassword)); contentAsNode.put("enabled", "true"); contentAsNode.put("service", "true"); @@ -291,7 +274,37 @@ public String generateAuthToken(String accountName) throws IOException { } } - public static void saveAndUpdateConfigs( + public void clearHash(String accountName) throws IOException { + final SecurityDynamicConfiguration internalUsersConfiguration = load(getUserConfigName(), false); + + if (!internalUsersConfiguration.exists(accountName)) { + throw new UserServiceException(FAILED_ACCOUNT_RETRIEVAL_MESSAGE); + } + + JsonNode accountDetails = DefaultObjectMapper.readTree(internalUsersConfiguration.getCEntry(accountName).toString()); + final ObjectNode contentAsNode = (ObjectNode) accountDetails; + SecurityJsonNode securityJsonNode = new SecurityJsonNode(contentAsNode); + + Optional.of(securityJsonNode.get("service")) + .map(SecurityJsonNode::asString) + .filter("true"::equalsIgnoreCase) + .orElseThrow(() -> new UserServiceException(FAILED_CLEAR_HASH_MESSAGE)); + + Optional.of(securityJsonNode.get("enabled")) + .map(SecurityJsonNode::asString) + .filter("true"::equalsIgnoreCase) + .orElseThrow(() -> new UserServiceException(FAILED_CLEAR_HASH_MESSAGE)); + + contentAsNode.remove("hash"); + contentAsNode.remove("name"); + internalUsersConfiguration.putCObject( + accountName, + DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass()) + ); + saveAndUpdateConfigs(getUserConfigName().toString(), client, CType.INTERNALUSERS, internalUsersConfiguration); + } + + public void saveAndUpdateConfigs( final String indexName, final Client client, final CType cType, @@ -314,4 +327,8 @@ public static void saveAndUpdateConfigs( throw ExceptionsHelper.convertToOpenSearchException(e); } } + + public SecurityDynamicConfiguration geInternalUsersConfigurationRepository() { + return load(getUserConfigName(), false); + } } diff --git a/src/main/java/org/opensearch/security/user/UserTokenHandler.java b/src/main/java/org/opensearch/security/user/UserTokenHandler.java new file mode 100644 index 0000000000..9dfe5e5efe --- /dev/null +++ b/src/main/java/org/opensearch/security/user/UserTokenHandler.java @@ -0,0 +1,171 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.user; + +import com.amazon.dlic.auth.http.jwt.keybyoidc.JwtVerifier; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer; +import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.opensearch.ExceptionsHelper; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.xcontent.XContentHelper; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.BearerAuthToken; +import org.opensearch.security.authtoken.jwt.JwtVendor; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.threadpool.ThreadPool; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; + +public class UserTokenHandler { + + private final int DEFAULT_EXPIRATION_TIME_SECONDS = 300; + JwtVendor jwtVendor; + Settings settings; + + ClusterService clusterService; + + ConfigurationRepository configurationRepository; + + ThreadPool threadPool; + + JwtVerifier jwtVerifier; + + String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + + String signingKey = RandomStringUtils.randomAlphanumeric(16); + + Client client; + + final static String FAILED_CLEAR_HASH_MESSAGE = "The hash could not be cleared from the specified account."; + + public static CType getRevokedTokensConfigName() { + return CType.REVOKEDTOKENS; + } + + @Inject + public UserTokenHandler( + ThreadPool threadPool, + ClusterService clusterService, + ConfigurationRepository configurationRepository, + Client client + ) { + this.settings = Settings.builder().put("signing_key", signingKey).put("encryption_key", claimsEncryptionKey).build(); + this.jwtVendor = new JwtVendor(settings, null); + this.threadPool = threadPool; + this.clusterService = clusterService; + this.client = client; + this.configurationRepository = configurationRepository; + } + + public AuthToken issueToken(String audience) { + ThreadContext threadContext = threadPool.getThreadContext(); + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + String jwt = null; + try { + jwt = jwtVendor.createJwt( + clusterService.getClusterName().toString(), + user.getName(), + audience, + DEFAULT_EXPIRATION_TIME_SECONDS, + new ArrayList(user.getRoles()), + null + ); + } catch (Exception e) { + throw new RuntimeException(e); + } + return new BearerAuthToken(jwt); + } + + public boolean validateToken(AuthToken authToken) { + if (!(authToken instanceof BearerAuthToken)) { + throw new UserServiceException("The provided token is not a BearerAuthToken."); + } + BearerAuthToken bearerAuthToken = (BearerAuthToken) authToken; + JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(bearerAuthToken.getCompleteToken()); + JwtToken jwt = jwtConsumer.getJwtToken(); + + Long iat = (Long) jwt.getClaim("iat"); + Long exp = (Long) jwt.getClaim("exp"); + SecurityDynamicConfiguration revokedTokens = load(getRevokedTokensConfigName(), false); + Long currentTime = System.currentTimeMillis(); + return (exp > currentTime && !revokedTokens.exists(bearerAuthToken.getCompleteToken())); + } + + public String getTokenInfo(AuthToken authToken) { + if (!(authToken instanceof BearerAuthToken)) { + throw new UserServiceException("The provided token is not a BearerAuthToken."); + } + BearerAuthToken bearerAuthToken = (BearerAuthToken) authToken; + return "The provided token is a BearerAuthToken with content: " + bearerAuthToken; + } + + public void revokeToken(AuthToken authToken) { + if (!(authToken instanceof BearerAuthToken)) { + throw new UserServiceException("The provided token is not a BearerAuthToken."); + } + BearerAuthToken bearerAuthToken = (BearerAuthToken) authToken; + SecurityDynamicConfiguration revokedTokens = load(getRevokedTokensConfigName(), false); + revokedTokens.putCObject(bearerAuthToken.getCompleteToken(), bearerAuthToken); + saveAndUpdateConfigs(getRevokedTokensConfigName().toString(), client, CType.REVOKEDTOKENS, revokedTokens); + } + + /** + * Load data for a given CType + * @param config CType whose data is to be loaded in-memory + * @return configuration loaded with given CType data + */ + public SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { + SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex( + Collections.singleton(config), + logComplianceEvent + ).get(config).deepClone(); + return DynamicConfigFactory.addStatics(loaded); + } + + public void saveAndUpdateConfigs( + final String indexName, + final Client client, + final CType cType, + final SecurityDynamicConfiguration configuration + ) { + final IndexRequest ir = new IndexRequest(indexName); + final String id = cType.toLCString(); + + configuration.removeStatic(); + + try { + client.index( + ir.id(id) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .setIfSeqNo(configuration.getSeqNo()) + .setIfPrimaryTerm(configuration.getPrimaryTerm()) + .source(id, XContentHelper.toXContent(configuration, XContentType.JSON, false)) + ); + } catch (IOException e) { + throw ExceptionsHelper.convertToOpenSearchException(e); + } + } +} From f6f368ea6065ba68f327fa1f254f69ddfbf99195 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 19 Jul 2023 15:08:50 -0700 Subject: [PATCH 2/6] Fix the NoSuchAlgo Exception Signed-off-by: Ryan Liang --- .../security/dlic/rest/api/InternalUsersApiAction.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java index 201d599117..e5bfb6d640 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.nio.file.Path; +import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Map; @@ -159,6 +160,8 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C return; } catch (IOException ex) { throw new IOException(ex); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); } // for existing users, hash is optional From b935c3281fa3b93ad2c94c5c536c3591b6b57cc4 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 27 Jul 2023 09:08:27 -0700 Subject: [PATCH 3/6] Add issueOBOToken in the Security Token Manager Signed-off-by: Ryan Liang --- .../identity/SecurityTokenManager.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index 11a5b994dc..502ce9fe9b 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -11,24 +11,40 @@ package org.opensearch.security.identity; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.apache.cxf.rs.security.jose.jwt.JwtConstants; +import org.greenrobot.eventbus.Subscribe; +import org.opensearch.OpenSearchSecurityException; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; +import org.opensearch.common.transport.TransportAddress; import org.opensearch.identity.tokens.AuthToken; import org.opensearch.identity.tokens.BasicAuthToken; import org.opensearch.identity.tokens.BearerAuthToken; import org.opensearch.identity.tokens.TokenManager; +import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.InternalUserTokenHandler; +import org.opensearch.security.user.User; import org.opensearch.security.user.UserService; import org.opensearch.security.user.UserServiceException; import org.opensearch.security.user.UserTokenHandler; import org.opensearch.threadpool.ThreadPool; +import org.apache.cxf.rs.security.jose.jwt.JwtConstants; /** * This class serves as a funneling implementation of the TokenManager interface. @@ -49,6 +65,9 @@ public class SecurityTokenManager implements TokenManager { InternalUserTokenHandler internalUserTokenHandler; public final String TOKEN_NOT_SUPPORTED_MESSAGE = "The provided token type is not supported by the Security Plugin."; + private ConfigModel configModel; + private DynamicConfigModel dcm; + private JwtVendor vendor; @Inject public SecurityTokenManager( @@ -83,6 +102,38 @@ public AuthToken issueToken(String account) { return token; } + @Override + public AuthToken issueOnBehalfOfToken(Map claims) { + String oboToken; + + User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + if (user == null && !claims.containsKey(JwtConstants.CLAIM_AUDIENCE)) { + throw new OpenSearchSecurityException("On-behalf-of Token cannot be issued due to the missing of subject/audience."); + } + + final TransportAddress caller = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + + String issuer = clusterService.getClusterName().value(); + String subject = user.getName(); + String audience = (String) claims.get(JwtConstants.CLAIM_AUDIENCE); + Integer expirySeconds = null; + List roles = new ArrayList<>(mapRoles(user, caller)); + List backendRoles = new ArrayList<>(user.getRoles()); + + try { + oboToken = vendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles); + } catch (Exception e) { + throw new RuntimeException(e); + } + + return new BearerAuthToken(oboToken); + } + + @Override + public AuthToken issueServiceAccountToken(String pluginOrExtensionPrincipal) { + return null; + } + public boolean validateToken(AuthToken authToken) { if (authToken instanceof BearerAuthToken) { @@ -143,4 +194,24 @@ protected final SecurityDynamicConfiguration load(final CType config, boolean ).get(config).deepClone(); return DynamicConfigFactory.addStatics(loaded); } + + public Set mapRoles(final User user, final TransportAddress caller) { + return this.configModel.mapSecurityRoles(user, caller); + } + + @Subscribe + public void onConfigModelChanged(ConfigModel configModel) { + this.configModel = configModel; + } + + @Subscribe + public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { + this.dcm = dcm; + if (dcm.getDynamicOnBehalfOfSettings().get("signing_key") != null + && dcm.getDynamicOnBehalfOfSettings().get("encryption_key") != null) { + this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty()); + } else { + this.vendor = null; + } + } } From 1be25512204afe4c51d23854ea1051dd432f620f Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 27 Jul 2023 15:51:21 -0700 Subject: [PATCH 4/6] Add test cases and modify the checks of obo validation Signed-off-by: Ryan Liang --- .../http/OnBehalfOfAuthenticator.java | 7 +- .../identity/SecurityTokenManager.java | 30 +++--- .../security/user/UserTokenHandler.java | 55 ++++++----- .../auth/SecurityTokenManagerTests.java | 93 +++++++++++++++++++ 4 files changed, 150 insertions(+), 35 deletions(-) create mode 100644 src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index f812cc97ee..9ef450d7fb 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -58,7 +58,6 @@ public class OnBehalfOfAuthenticator implements HTTPAuthenticator { private static final String BEARER_PREFIX = "bearer "; private static final String TOKEN_TYPE_CLAIM = "typ"; private static final String TOKEN_TYPE = "obo"; - private final JwtParser jwtParser; private final String encryptionKey; private final Boolean oboEnabled; @@ -191,6 +190,12 @@ private AuthCredentials extractCredentials0(final RestRequest request) { final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); + final String tokenType = claims.get(TOKEN_TYPE_CLAIM).toString(); + if (!tokenType.equals(TOKEN_TYPE)) { + log.error("This token is not verifying as an on-behalf-of token"); + return null; + } + final String subject = claims.getSubject(); if (Objects.isNull(subject)) { log.error("Valid jwt on behalf of token with no subject"); diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index 502ce9fe9b..aa70a5c84d 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -18,7 +18,9 @@ import java.util.Optional; import java.util.Set; +import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer; import org.apache.cxf.rs.security.jose.jwt.JwtConstants; +import org.apache.cxf.rs.security.jose.jwt.JwtToken; import org.greenrobot.eventbus.Subscribe; import org.opensearch.OpenSearchSecurityException; import org.opensearch.client.Client; @@ -89,19 +91,6 @@ public SecurityTokenManager( } - @Override - public AuthToken issueToken(String account) { - - AuthToken token; - final SecurityDynamicConfiguration internalUsersConfiguration = load(UserService.getUserConfigName(), false); - if (internalUsersConfiguration.exists(account)) { - token = internalUserTokenHandler.issueToken(account); - } else { - token = userTokenHandler.issueToken(account); - } - return token; - } - @Override public AuthToken issueOnBehalfOfToken(Map claims) { String oboToken; @@ -129,6 +118,7 @@ public AuthToken issueOnBehalfOfToken(Map claims) { return new BearerAuthToken(oboToken); } + // TODO: IMPLEMENT ISSUE SERVICE ACCOUNT INTERFACE FIRST @Override public AuthToken issueServiceAccountToken(String pluginOrExtensionPrincipal) { return null; @@ -137,8 +127,15 @@ public AuthToken issueServiceAccountToken(String pluginOrExtensionPrincipal) { public boolean validateToken(AuthToken authToken) { if (authToken instanceof BearerAuthToken) { + String encodedTokenString = authToken.getTokenValue(); + JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedTokenString); + JwtToken jwt = jwtConsumer.getJwtToken(); + if ("obo".equals(jwt.getClaim("typ"))) { + return userTokenHandler.validateJustInTimeToken(authToken); + } return userTokenHandler.validateToken(authToken); } + if (authToken instanceof BasicAuthToken) { return internalUserTokenHandler.validateToken(authToken); } @@ -168,6 +165,13 @@ public void revokeToken(AuthToken authToken) { throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE); } + /** + * Only for testing + */ + public void setJwtVendor(JwtVendor vendor) { + this.vendor = vendor; + } + /** * Only for testing */ diff --git a/src/main/java/org/opensearch/security/user/UserTokenHandler.java b/src/main/java/org/opensearch/security/user/UserTokenHandler.java index 9dfe5e5efe..5985fc1848 100644 --- a/src/main/java/org/opensearch/security/user/UserTokenHandler.java +++ b/src/main/java/org/opensearch/security/user/UserTokenHandler.java @@ -22,7 +22,6 @@ import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.common.xcontent.XContentHelper; import org.opensearch.common.xcontent.XContentType; import org.opensearch.identity.tokens.AuthToken; @@ -32,11 +31,9 @@ import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; -import org.opensearch.security.support.ConfigConstants; import org.opensearch.threadpool.ThreadPool; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; public class UserTokenHandler { @@ -80,24 +77,24 @@ public UserTokenHandler( this.configurationRepository = configurationRepository; } - public AuthToken issueToken(String audience) { - ThreadContext threadContext = threadPool.getThreadContext(); - final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - String jwt = null; - try { - jwt = jwtVendor.createJwt( - clusterService.getClusterName().toString(), - user.getName(), - audience, - DEFAULT_EXPIRATION_TIME_SECONDS, - new ArrayList(user.getRoles()), - null - ); - } catch (Exception e) { - throw new RuntimeException(e); - } - return new BearerAuthToken(jwt); - } + // public AuthToken issueToken(String audience) { + // ThreadContext threadContext = threadPool.getThreadContext(); + // final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + // String jwt = null; + // try { + // jwt = jwtVendor.createJwt( + // clusterService.getClusterName().toString(), + // user.getName(), + // audience, + // DEFAULT_EXPIRATION_TIME_SECONDS, + // new ArrayList(user.getRoles()), + // null + // ); + // } catch (Exception e) { + // throw new RuntimeException(e); + // } + // return new BearerAuthToken(jwt); + // } public boolean validateToken(AuthToken authToken) { if (!(authToken instanceof BearerAuthToken)) { @@ -114,6 +111,22 @@ public boolean validateToken(AuthToken authToken) { return (exp > currentTime && !revokedTokens.exists(bearerAuthToken.getCompleteToken())); } + public boolean validateJustInTimeToken(AuthToken authToken) { + if (!(authToken instanceof BearerAuthToken)) { + throw new UserServiceException("The provided token is not a BearerAuthToken."); + } + BearerAuthToken bearerAuthToken = (BearerAuthToken) authToken; + JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(bearerAuthToken.getCompleteToken()); + JwtToken jwt = jwtConsumer.getJwtToken(); + + Long iat = (Long) jwt.getClaim("iat"); + Long nbf = (Long) jwt.getClaim("nbf"); + Long exp = (Long) jwt.getClaim("exp"); + SecurityDynamicConfiguration revokedTokens = load(getRevokedTokensConfigName(), false); + Long currentTime = System.currentTimeMillis(); + return (iat == nbf && exp > currentTime && !revokedTokens.exists(bearerAuthToken.getCompleteToken())); + } + public String getTokenInfo(AuthToken authToken) { if (!(authToken instanceof BearerAuthToken)) { throw new UserServiceException("The provided token is not a BearerAuthToken."); diff --git a/src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java b/src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java new file mode 100644 index 0000000000..b4f42b99d3 --- /dev/null +++ b/src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java @@ -0,0 +1,93 @@ +package org.opensearch.security.auth; + +import com.google.common.io.BaseEncoding; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer; +import org.apache.cxf.rs.security.jose.jwt.JwtConstants; +import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockitoAnnotations; + +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.security.authtoken.jwt.JwtVendor; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.identity.SecurityTokenManager; +import org.opensearch.security.user.InternalUserTokenHandler; +import org.opensearch.security.user.UserService; +import org.opensearch.security.user.UserTokenHandler; +import org.opensearch.threadpool.ThreadPool; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +public class SecurityTokenManagerTests { + + final static String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + final static String signingKey = + "This is my super safe signing key that no one will ever be able to guess. It's would take billions of years and the world's most powerful quantum computer to crack"; + final static String signingKeyB64Encoded = BaseEncoding.base64().encode(signingKey.getBytes(StandardCharsets.UTF_8)); + + SecurityTokenManager securityTokenManager; + private UserTokenHandler userTokenhandler; + private InternalUserTokenHandler internalUserTOkenHandler; + private ClusterService clusterService; + private Map claims; + + UserService userService; + + @Before + public void setup() { + claims = new HashMap() { + { + put(JwtConstants.CLAIM_AUDIENCE, "ext_0"); + } + }; + MockitoAnnotations.openMocks(this); + Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); + Client client = mock(Client.class); + ThreadPool threadPool = mock(ThreadPool.class); + ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); + clusterService = mock(ClusterService.class); + userService = mock(UserService.class); + securityTokenManager = spy( + new SecurityTokenManager(threadPool, clusterService, configurationRepository, client, settings, userService) + ); + userTokenhandler = mock(UserTokenHandler.class); + internalUserTOkenHandler = mock(InternalUserTokenHandler.class); + securityTokenManager.setJwtVendor(jwtVendor); + securityTokenManager.setInternalUserTokenHandler(internalUserTOkenHandler); + securityTokenManager.setUserTokenHandler(userTokenhandler); + } + + @Test + public void testIssueOnBehalfOfTokenShouldPass() { + AuthToken createdOnBehalfOfAuthToken = securityTokenManager.issueOnBehalfOfToken(claims); + + String encodedOBOTokenString = createdOnBehalfOfAuthToken.getTokenValue(); + JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedOBOTokenString); + JwtToken jwt = jwtConsumer.getJwtToken(); + + Assert.assertEquals("obo", jwt.getClaim("typ")); + } + + @Test + public void testValidateOnBehalfOfTokenShouldPass() { + AuthToken createdOnBehalfOfToken = securityTokenManager.issueOnBehalfOfToken(claims); + doReturn(true).when(userTokenhandler).validateJustInTimeToken(createdOnBehalfOfToken); + + Assert.assertTrue(securityTokenManager.validateToken(createdOnBehalfOfToken)); + + } +} From 69dfb673b7720d55057d18640785fa90ec3c22c9 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 27 Jul 2023 15:54:44 -0700 Subject: [PATCH 5/6] Add license header for security token manager tests Signed-off-by: Ryan Liang --- .../security/auth/SecurityTokenManagerTests.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java b/src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java index b4f42b99d3..bbb4c72c3e 100644 --- a/src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java +++ b/src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java @@ -1,3 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + package org.opensearch.security.auth; import com.google.common.io.BaseEncoding; From 03a7875ff6ed83cf4cdc3e8a8ae4c16e0099d5cf Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Tue, 8 Aug 2023 10:56:49 -0700 Subject: [PATCH 6/6] Add library fix Signed-off-by: Ryan Liang --- .../org/opensearch/security/identity/SecurityTokenManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index aa70a5c84d..e991d8458f 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -27,7 +27,7 @@ import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; -import org.opensearch.common.transport.TransportAddress; +import org.opensearch.core.common.transport.TransportAddress; import org.opensearch.identity.tokens.AuthToken; import org.opensearch.identity.tokens.BasicAuthToken; import org.opensearch.identity.tokens.BearerAuthToken;