summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVarun Ravichandran <varun.ravichandran@mongodb.com>2023-01-17 01:37:56 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2023-01-23 15:16:44 +0000
commit23102a1982d8eaccb4bd7aa83a314699892fdf5d (patch)
tree642ae9f2aba328ed9e3006d7180c979eb970b1e7
parent053984fd853e1900c4558006d2ed577789648142 (diff)
downloadmongo-23102a1982d8eaccb4bd7aa83a314699892fdf5d.tar.gz
SERVER-71195: Refresh JWKSets periodically and add key management commands
-rw-r--r--jstests/auth/lib/commands_lib.js36
-rw-r--r--jstests/core/views/views_all_commands.js2
-rw-r--r--jstests/noPassthrough/comment_field_passthrough.js12
-rw-r--r--jstests/replsets/all_commands_downgrading_to_upgraded.js6
-rw-r--r--jstests/replsets/db_reads_while_recovering_all_commands.js2
-rw-r--r--jstests/sharding/database_versioning_all_commands.js2
-rw-r--r--jstests/sharding/libs/last_lts_mongod_commands.js2
-rw-r--r--jstests/sharding/libs/last_lts_mongos_commands.js2
-rw-r--r--jstests/sharding/read_write_concern_defaults_application.js2
-rw-r--r--jstests/sharding/safe_secondary_reads_drop_recreate.js2
-rw-r--r--jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js2
-rw-r--r--jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js2
-rw-r--r--src/mongo/crypto/jwk_manager.cpp118
-rw-r--r--src/mongo/crypto/jwk_manager.h43
-rw-r--r--src/mongo/crypto/jws_validated_token.cpp18
-rw-r--r--src/mongo/crypto/jws_validated_token.h4
-rw-r--r--src/mongo/crypto/jwt_test.cpp13
-rw-r--r--src/mongo/db/auth/action_type.idl2
-rw-r--r--src/mongo/db/auth/authorization_manager_impl.cpp8
-rw-r--r--src/mongo/db/auth/authorization_manager_impl.h1
-rw-r--r--src/mongo/db/auth/authorization_session_impl.cpp23
-rw-r--r--src/mongo/db/auth/builtin_roles.yml2
22 files changed, 219 insertions, 85 deletions
diff --git a/jstests/auth/lib/commands_lib.js b/jstests/auth/lib/commands_lib.js
index 7a019f71333..3ffeac6d8ba 100644
--- a/jstests/auth/lib/commands_lib.js
+++ b/jstests/auth/lib/commands_lib.js
@@ -5288,6 +5288,42 @@ export const authCommandsLib = {
]
},
{
+ testname: "oidcListKeys",
+ command: {oidcListKeys: 1},
+ // Only enterprise knows of this command.
+ skipTest:
+ (conn) => {
+ return !getBuildInfo().modules.includes("enterprise")
+ || !TestData.setParameters.featureFlagOIDC;
+ },
+ testcases: [
+ {
+ runOnDb: adminDbName,
+ roles: roles_hostManager,
+ privileges: [{resource: {cluster: true}, actions: ["oidcListKeys"]}],
+ expectFail: true, // Server isn't configured for MONGODB-OIDC as an auth mechanism.
+ }
+ ]
+ },
+ {
+ testname: "oidcRefreshKeys",
+ command: {oidcRefreshKeys: 1},
+ // Only enterprise knows of this command.
+ skipTest:
+ (conn) => {
+ return !getBuildInfo().modules.includes("enterprise")
+ || !TestData.setParameters.featureFlagOIDC;
+ },
+ testcases: [
+ {
+ runOnDb: adminDbName,
+ roles: roles_hostManager,
+ privileges: [{resource: {cluster: true}, actions: ["oidcRefreshKeys"]}],
+ expectFail: true, // Server isn't figured for MONGODB-OIDC as an auth mechanism.
+ }
+ ]
+ },
+ {
testname: "planCacheIndexFilter",
command: {planCacheClearFilters: "x"},
skipSharded: true,
diff --git a/jstests/core/views/views_all_commands.js b/jstests/core/views/views_all_commands.js
index 2eb0d3471ea..0d2d1e88263 100644
--- a/jstests/core/views/views_all_commands.js
+++ b/jstests/core/views/views_all_commands.js
@@ -529,6 +529,8 @@ let viewsCommandTests = {
moveRange: {skip: isUnrelated},
multicast: {skip: isUnrelated},
netstat: {skip: isAnInternalCommand},
+ oidcListKeys: {skip: isUnrelated},
+ oidcRefreshKeys: {skip: isUnrelated},
pinHistoryReplicated: {skip: isAnInternalCommand},
ping: {command: {ping: 1}},
planCacheClear: {command: {planCacheClear: "view"}, expectFailure: true},
diff --git a/jstests/noPassthrough/comment_field_passthrough.js b/jstests/noPassthrough/comment_field_passthrough.js
index 7af96170f23..1492a3ae702 100644
--- a/jstests/noPassthrough/comment_field_passthrough.js
+++ b/jstests/noPassthrough/comment_field_passthrough.js
@@ -8,15 +8,21 @@
* ]
*/
-import {authCommandsLib, firstDbName, secondDbName} from "jstests/auth/lib/commands_lib.js";
+import {authCommandsLib} from "jstests/auth/lib/commands_lib.js";
load("jstests/libs/fail_point_util.js"); // Helper to enable/disable failpoints easily.
const tests = authCommandsLib.tests;
// The following commands require additional start up configuration and hence need to be skipped.
-const denylistedTests =
- ["startRecordingTraffic", "stopRecordingTraffic", "addShardToZone", "removeShardFromZone"];
+const denylistedTests = [
+ "startRecordingTraffic",
+ "stopRecordingTraffic",
+ "addShardToZone",
+ "removeShardFromZone",
+ "oidcListKeys",
+ "oidcRefreshKeys"
+];
function runTests(tests, conn, impls, options) {
for (const test of tests) {
diff --git a/jstests/replsets/all_commands_downgrading_to_upgraded.js b/jstests/replsets/all_commands_downgrading_to_upgraded.js
index 9cfc50916f9..cc70c7e10a5 100644
--- a/jstests/replsets/all_commands_downgrading_to_upgraded.js
+++ b/jstests/replsets/all_commands_downgrading_to_upgraded.js
@@ -899,6 +899,12 @@ const allCommands = {
skip: isNotImplementedYet,
isShardedOnly: true,
},
+ oidcListKeys: {
+ skip: isNotImplementedYet,
+ },
+ oidcRefreshKeys: {
+ skip: isNotImplementedYet,
+ },
pinHistoryReplicated: {
skip: isNotImplementedYet,
},
diff --git a/jstests/replsets/db_reads_while_recovering_all_commands.js b/jstests/replsets/db_reads_while_recovering_all_commands.js
index 8dd34026cca..cea7fd73b3c 100644
--- a/jstests/replsets/db_reads_while_recovering_all_commands.js
+++ b/jstests/replsets/db_reads_while_recovering_all_commands.js
@@ -325,6 +325,8 @@ const allCommands = {
mergeChunks: {skip: isPrimaryOnly},
moveChunk: {skip: isPrimaryOnly},
moveRange: {skip: isPrimaryOnly},
+ oidcListKeys: {skip: isNotAUserDataRead},
+ oidcRefreshKeys: {skip: isNotAUserDataRead},
pinHistoryReplicated: {skip: isAnInternalCommand},
ping: {skip: isNotAUserDataRead},
planCacheClear: {skip: isNotAUserDataRead},
diff --git a/jstests/sharding/database_versioning_all_commands.js b/jstests/sharding/database_versioning_all_commands.js
index 8248caf83bd..475408a4a4e 100644
--- a/jstests/sharding/database_versioning_all_commands.js
+++ b/jstests/sharding/database_versioning_all_commands.js
@@ -583,6 +583,8 @@ let testCases = {
moveRange: {skip: "does not forward command to primary shard"},
multicast: {skip: "does not forward command to primary shard"},
netstat: {skip: "executes locally on mongos (not sent to any remote node)"},
+ oidcListKeys: {skip: "executes locally on mongos (not sent to any remote node)"},
+ oidcRefreshKeys: {skip: "executes locally on mongos (not sent to any remote node)"},
ping: {skip: "executes locally on mongos (not sent to any remote node)"},
planCacheClear: {
run: {
diff --git a/jstests/sharding/libs/last_lts_mongod_commands.js b/jstests/sharding/libs/last_lts_mongod_commands.js
index 8b71181063d..134247d8e6e 100644
--- a/jstests/sharding/libs/last_lts_mongod_commands.js
+++ b/jstests/sharding/libs/last_lts_mongod_commands.js
@@ -31,6 +31,8 @@ const commandsAddedToMongodSinceLastLTS = [
"getChangeStreamState",
"getClusterParameter",
"listDatabasesForAllTenants",
+ "oidcListKeys",
+ "oidcRefreshKeys",
"rotateCertificates",
"setChangeStreamState",
"setClusterParameter",
diff --git a/jstests/sharding/libs/last_lts_mongos_commands.js b/jstests/sharding/libs/last_lts_mongos_commands.js
index 97ae0116371..56e3870df6c 100644
--- a/jstests/sharding/libs/last_lts_mongos_commands.js
+++ b/jstests/sharding/libs/last_lts_mongos_commands.js
@@ -31,6 +31,8 @@ const commandsAddedToMongosSinceLastLTS = [
"getClusterParameter",
"mergeAllChunksOnShard",
"moveRange",
+ "oidcListKeys",
+ "oidcRefreshKeys",
"reshardCollection",
"rotateCertificates",
"setAllowMigrations",
diff --git a/jstests/sharding/read_write_concern_defaults_application.js b/jstests/sharding/read_write_concern_defaults_application.js
index e63b757edfd..db3e3835178 100644
--- a/jstests/sharding/read_write_concern_defaults_application.js
+++ b/jstests/sharding/read_write_concern_defaults_application.js
@@ -584,6 +584,8 @@ let testCases = {
},
multicast: {skip: "does not accept read or write concern"},
netstat: {skip: "internal command"},
+ oidcListKeys: {skip: "does not accept read or write concern"},
+ oidcRefreshKeys: {skip: "does not accept read or write concern"},
pinHistoryReplicated: {skip: "internal command"},
ping: {skip: "does not accept read or write concern"},
planCacheClear: {skip: "does not accept read or write concern"},
diff --git a/jstests/sharding/safe_secondary_reads_drop_recreate.js b/jstests/sharding/safe_secondary_reads_drop_recreate.js
index c89198ff0b4..50cbfbf9c26 100644
--- a/jstests/sharding/safe_secondary_reads_drop_recreate.js
+++ b/jstests/sharding/safe_secondary_reads_drop_recreate.js
@@ -276,6 +276,8 @@ let testCases = {
moveRange: {skip: "primary only"},
multicast: {skip: "does not return user data"},
netstat: {skip: "does not return user data"},
+ oidcListKeys: {skip: "does not return user data"},
+ oidcRefreshKeys: {skip: "does not return user data"},
ping: {skip: "does not return user data"},
planCacheClear: {skip: "does not return user data"},
planCacheClearFilters: {skip: "does not return user data"},
diff --git a/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js b/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js
index c3a5f64b711..3017f7c76a3 100644
--- a/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js
+++ b/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js
@@ -347,6 +347,8 @@ let testCases = {
moveRange: {skip: "primary only"},
multicast: {skip: "does not return user data"},
netstat: {skip: "does not return user data"},
+ oidcListKeys: {skip: "does not return user data"},
+ oidcRefreshKeys: {skip: "does not return user data"},
ping: {skip: "does not return user data"},
planCacheClear: {skip: "does not return user data"},
planCacheClearFilters: {skip: "does not return user data"},
diff --git a/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js b/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js
index 54ca9a2d7c9..790774169cb 100644
--- a/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js
+++ b/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js
@@ -283,6 +283,8 @@ let testCases = {
moveRange: {skip: "primary only"},
multicast: {skip: "does not return user data"},
netstat: {skip: "does not return user data"},
+ oidcListKeys: {skip: "does not return user data"},
+ oidcRefreshKeys: {skip: "does not return user data"},
ping: {skip: "does not return user data"},
planCacheClear: {skip: "does not return user data"},
planCacheClearFilters: {skip: "does not return user data"},
diff --git a/src/mongo/crypto/jwk_manager.cpp b/src/mongo/crypto/jwk_manager.cpp
index 8291108d9f0..2465cf63f2d 100644
--- a/src/mongo/crypto/jwk_manager.cpp
+++ b/src/mongo/crypto/jwk_manager.cpp
@@ -43,7 +43,7 @@ namespace mongo::crypto {
namespace {
constexpr auto kMinKeySizeBytes = 2048 >> 3;
using SharedValidator = std::shared_ptr<JWSValidator>;
-using SharedMap = std::map<std::string, SharedValidator>;
+using SharedValidatorMap = std::map<std::string, SharedValidator>;
// Strip insignificant leading zeroes to determine the key's true size.
StringData reduceInt(StringData value) {
@@ -57,51 +57,36 @@ StringData reduceInt(StringData value) {
} // namespace
JWKManager::JWKManager(StringData source) : _keyURI(source) {
- try {
- auto httpClient = HttpClient::createWithoutConnectionPool();
- httpClient->setHeaders({"Accept: */*"});
- httpClient->allowInsecureHTTP(getTestCommandsEnabled());
-
- auto getJWKs = httpClient->get(source);
-
- ConstDataRange cdr = getJWKs.getCursor();
- StringData str;
- cdr.readInto<StringData>(&str);
-
- BSONObj data = fromjson(str);
- _setAndValidateKeys(data);
- } catch (const DBException& ex) {
- // throws
- uassertStatusOK(
- ex.toStatus().withContext(str::stream() << "Failed loading keys from " << source));
- }
+ _loadKeysFromUri(true /* isInitialLoad */);
}
JWKManager::JWKManager(BSONObj keys) {
- _setAndValidateKeys(keys);
+ _setAndValidateKeys(keys, true /* isInitialLoad */);
}
-StatusWith<BSONObj> JWKManager::getKey(StringData keyId) const {
- auto it = _keyMaterial.find(keyId.toString());
- if (it == _keyMaterial.end()) {
- return {ErrorCodes::NoSuchKey, str::stream() << "Unknown key '" << keyId << "'"};
+StatusWith<SharedValidator> JWKManager::getValidator(StringData keyId) {
+ auto currentValidators = _validators;
+ auto it = currentValidators->find(keyId.toString());
+ if (it == currentValidators->end()) {
+ // If the JWKManager has been initialized with an URI, try refreshing.
+ if (_keyURI) {
+ _loadKeysFromUri(false /* isInitialLoad */);
+ currentValidators = _validators;
+ it = currentValidators->find(keyId.toString());
+ }
+
+ // If it still cannot be found, return an error.
+ if (it == currentValidators->end()) {
+ return {ErrorCodes::NoSuchKey, str::stream() << "Unknown key '" << keyId << "'"};
+ }
}
return it->second;
}
-SharedValidator JWKManager::getValidator(StringData keyId) const {
- auto it = _validators->find(keyId.toString());
+void JWKManager::_setAndValidateKeys(const BSONObj& keys, bool isInitialLoad) {
+ auto newValidators = std::make_shared<SharedValidatorMap>();
+ auto newKeyMaterial = std::make_shared<KeyMap>();
- // TODO: SERVER-71195, refresh keys from the endpoint and try to get the validator again.
- // If still no key is found throw a uassert.
- uassert(ErrorCodes::NoSuchKey,
- str::stream() << "Unknown key '" << keyId << "'",
- it != _validators->end());
- return it->second;
-}
-
-void JWKManager::_setAndValidateKeys(const BSONObj& keys) {
- _validators = std::make_shared<SharedMap>();
auto keysParsed = JWKSet::parse(IDLParserContext("JWKSet"), keys);
for (const auto& key : keysParsed.getKeys()) {
@@ -130,27 +115,66 @@ void JWKManager::_setAndValidateKeys(const BSONObj& keys) {
auto keyId = RSAKey.getKeyId().toString();
uassert(ErrorCodes::DuplicateKeyId,
str::stream() << "Key IDs must be unique, duplicate '" << keyId << "'",
- _keyMaterial.find(keyId) == _keyMaterial.end());
+ newKeyMaterial->find(keyId) == newKeyMaterial->end());
- LOGV2_DEBUG(6766000, 5, "Loaded JWK Key", "kid"_attr = RSAKey.getKeyId());
- _keyMaterial.insert({keyId, key.copy()});
+ // Does not need to be loaded atomically because isInitialLoad is only true when invoked
+ // from the constructor, so there will not be any concurrent reads.
+ if (isInitialLoad) {
+ _initialKeyMaterial.insert({keyId, key.copy()});
+ }
+
+ newKeyMaterial->insert({keyId, key.copy()});
auto swValidator = JWSValidator::create(JWK.getType(), key);
uassertStatusOK(swValidator.getStatus());
SharedValidator shValidator = std::move(swValidator.getValue());
- _validators->insert({keyId, shValidator});
+ newValidators->insert({keyId, shValidator});
LOGV2_DEBUG(7070202, 3, "Loaded JWK key", "kid"_attr = keyId, "typ"_attr = JWK.getType());
}
+
+ // A mutex is not used here because no single thread consumes both _validators and _keyMaterial.
+ // _keyMaterial is used purely for reflection of current key state while _validators is used
+ // for actual signature verification with the keys. Therefore, it's safe to update each
+ // atomically rather than both under a mutex.
+ std::atomic_exchange(&_validators, std::move(newValidators)); // NOLINT
+ std::atomic_exchange(&_keyMaterial, std::move(newKeyMaterial)); // NOLINT
}
-std::vector<std::string> JWKManager::getKeyIds() const {
- std::vector<std::string> ids;
- std::transform(_validators->cbegin(),
- _validators->cend(),
- std::back_inserter(ids),
- [](const auto& it) { return it.first; });
- return ids;
+void JWKManager::_loadKeysFromUri(bool isInitialLoad) {
+ try {
+ auto httpClient = HttpClient::createWithoutConnectionPool();
+ httpClient->setHeaders({"Accept: */*"});
+ httpClient->allowInsecureHTTP(getTestCommandsEnabled());
+
+ invariant(_keyURI);
+ auto getJWKs = httpClient->get(_keyURI.value());
+
+ ConstDataRange cdr = getJWKs.getCursor();
+ StringData str;
+ cdr.readInto<StringData>(&str);
+
+ BSONObj data = fromjson(str);
+ _setAndValidateKeys(data, isInitialLoad);
+ } catch (const DBException& ex) {
+ // throws
+ uassertStatusOK(ex.toStatus().withContext(str::stream() << "Failed loading keys from "
+ << _keyURI.value()));
+ }
+}
+
+void JWKManager::serialize(BSONObjBuilder* bob) const {
+ std::vector<BSONObj> keyVector;
+ keyVector.reserve(size());
+
+ auto currentKeys = _keyMaterial;
+ std::transform(currentKeys->begin(),
+ currentKeys->end(),
+ std::back_inserter(keyVector),
+ [](const auto& keyEntry) { return keyEntry.second; });
+
+ JWKSet jwks(keyVector);
+ jwks.serialize(bob);
}
} // namespace mongo::crypto
diff --git a/src/mongo/crypto/jwk_manager.h b/src/mongo/crypto/jwk_manager.h
index bba9a276388..03a05cb1ee8 100644
--- a/src/mongo/crypto/jwk_manager.h
+++ b/src/mongo/crypto/jwk_manager.h
@@ -42,6 +42,7 @@ namespace mongo::crypto {
class JWKManager {
public:
using SharedValidator = std::shared_ptr<JWSValidator>;
+ using KeyMap = std::map<std::string, BSONObj>;
/**
* Fetch a JWKS file from the specified URL, parse them as keys,
@@ -56,36 +57,44 @@ public:
explicit JWKManager(BSONObj keys);
/**
- * Given a unique keyId it will return the matching JWK.
- * If no key is found for the given keyId, a
- */
- StatusWith<BSONObj> getKey(StringData keyId) const;
-
- /**
* Fetch a specific JWSValidator from the JWKManager by keyId.
- * If the keyId does not exist,
- * a rate-limited refresh of the JWK endpoint will be
- * triggered.
- * If the keyId still does not exist, return ptr will be empty.
+ * If the keyId does not exist, it will refresh _keyMaterial and _validators and retry.
+ * If it still cannot be found, it will return an error.
*/
- SharedValidator getValidator(StringData keyId) const;
+ StatusWith<SharedValidator> getValidator(StringData keyId);
std::size_t size() const {
return _validators->size();
}
/**
- * Get the list of keyIds held by this key manager.
+ * Get a snapshot of the keys the key manager was initialized with. It is not
+ * modified after initialization and can be used to determine whether
+ * invalidation is necessary.
+ * TODO SERVER-73165 Generalize this to better handle invalidation.
+ */
+ const KeyMap& getInitialKeys() const {
+ return _initialKeyMaterial;
+ }
+
+ /**
+ * Serialize the JWKs stored in this key manager into the BSONObjBuilder.
*/
- std::vector<std::string> getKeyIds() const;
+ void serialize(BSONObjBuilder* bob) const;
private:
- void _setAndValidateKeys(const BSONObj& keys);
+ void _setAndValidateKeys(const BSONObj& keys, bool isInitialLoad);
+
+ void _loadKeysFromUri(bool isInitialLoad);
private:
- // Map<keyId, JWKRSA>
- std::map<std::string, BSONObj> _keyMaterial; // From last load
- std::string _keyURI;
+ // Stores the key material that the manager was initialized with.
+ KeyMap _initialKeyMaterial;
+
+ // Stores the current key material of the manager, which may have been refreshed.
+ std::shared_ptr<KeyMap> _keyMaterial;
+
+ boost::optional<std::string> _keyURI;
std::shared_ptr<std::map<std::string, SharedValidator>> _validators;
};
diff --git a/src/mongo/crypto/jws_validated_token.cpp b/src/mongo/crypto/jws_validated_token.cpp
index 6bd0713e0c6..e8a0738e905 100644
--- a/src/mongo/crypto/jws_validated_token.cpp
+++ b/src/mongo/crypto/jws_validated_token.cpp
@@ -64,7 +64,7 @@ ParsedToken parseSignedToken(StringData token) {
}
} // namespace
-Status JWSValidatedToken::validate(const JWKManager& keyMgr) const {
+Status JWSValidatedToken::validate(JWKManager* keyMgr) const {
const auto now = Date_t::now();
// Clock times across the network may differ and `nbf` is likely to be
@@ -86,16 +86,20 @@ Status JWSValidatedToken::validate(const JWKManager& keyMgr) const {
auto signature = base64url::decode(tokenSplit.token[2]);
auto payload = tokenSplit.payload;
- auto sValidator = keyMgr.getValidator(_header.getKeyId());
- if (!sValidator) {
- return Status{ErrorCodes::BadValue,
- str::stream() << "Unknown JWT keyId << '" << _header.getKeyId() << "'"};
+ auto swValidator = keyMgr->getValidator(_header.getKeyId());
+ if (!swValidator.isOK()) {
+ auto status = swValidator.getStatus();
+ if (status.code() == ErrorCodes::NoSuchKey) {
+ return Status{ErrorCodes::BadValue,
+ str::stream() << "Unknown JWT keyId << '" << _header.getKeyId() << "'"};
+ }
+ return status;
}
- return sValidator.get()->validate(_header.getAlgorithm(), payload, signature);
+ return swValidator.getValue().get()->validate(_header.getAlgorithm(), payload, signature);
}
-JWSValidatedToken::JWSValidatedToken(const JWKManager& keyMgr, StringData token)
+JWSValidatedToken::JWSValidatedToken(JWKManager* keyMgr, StringData token)
: _originalToken(token.toString()) {
auto tokenSplit = parseSignedToken(token);
diff --git a/src/mongo/crypto/jws_validated_token.h b/src/mongo/crypto/jws_validated_token.h
index 1f304e37072..58474ec3e17 100644
--- a/src/mongo/crypto/jws_validated_token.h
+++ b/src/mongo/crypto/jws_validated_token.h
@@ -46,7 +46,7 @@ public:
* Upon completion, header and body payloads
* and parsed structs are available.
*/
- JWSValidatedToken(const JWKManager& keyMgr, StringData token);
+ JWSValidatedToken(JWKManager* keyMgr, StringData token);
/**
* Extract just the Issuer name ('iss') from the token.
@@ -58,7 +58,7 @@ public:
* verifies it has a validator matching its keyId and finally
* it calls validate from the validator, returning the status.
*/
- Status validate(const JWKManager& keyMgr) const;
+ Status validate(JWKManager* keyMgr) const;
// General read-only accessors.
const BSONObj& getHeaderBSON() const {
diff --git a/src/mongo/crypto/jwt_test.cpp b/src/mongo/crypto/jwt_test.cpp
index b43b929b08a..ae414ae17f0 100644
--- a/src/mongo/crypto/jwt_test.cpp
+++ b/src/mongo/crypto/jwt_test.cpp
@@ -53,13 +53,20 @@ TEST(JWKManager, parseJWKSetBasicFromSource) {
BSONObj data = fromjson(str);
JWKManager manager(source);
+ BSONObjBuilder bob;
+ manager.serialize(&bob);
+ ASSERT_BSONOBJ_EQ(bob.obj(), data);
+
+ const auto& initialKeys = manager.getInitialKeys();
for (const auto& key : data["keys"_sd].Obj()) {
- auto keyFromKid = uassertStatusOK(manager.getKey(key["kid"_sd].str()));
- ASSERT_BSONOBJ_EQ(key.Obj(), keyFromKid);
+ auto initialKey = initialKeys.find(key["kid"_sd].str());
+ ASSERT(initialKey != initialKeys.end());
+ ASSERT_BSONOBJ_EQ(key.Obj(), initialKey->second);
}
for (const auto& key : data["keys"_sd].Obj()) {
- ASSERT(manager.getValidator(key["kid"_sd].str()));
+ auto validator = uassertStatusOK(manager.getValidator(key["kid"_sd].str()));
+ ASSERT(validator);
}
}
diff --git a/src/mongo/db/auth/action_type.idl b/src/mongo/db/auth/action_type.idl
index 172c6810026..9ef3ca73a98 100644
--- a/src/mongo/db/auth/action_type.idl
+++ b/src/mongo/db/auth/action_type.idl
@@ -132,6 +132,8 @@ enums:
logRotate : "logRotate"
moveChunk : "moveChunk"
netstat : "netstat"
+ oidcListKeys : "oidcListKeys"
+ oidcRefreshKeys : "oidcRefreshKeys"
oidReset : "oidReset" # machine ID reset via the features command
operationMetrics : "operationMetrics"
planCacheIndexFilter : "planCacheIndexFilter" # view/update index filters
diff --git a/src/mongo/db/auth/authorization_manager_impl.cpp b/src/mongo/db/auth/authorization_manager_impl.cpp
index 0da6eeef244..c7dd4218fce 100644
--- a/src/mongo/db/auth/authorization_manager_impl.cpp
+++ b/src/mongo/db/auth/authorization_manager_impl.cpp
@@ -569,7 +569,13 @@ StatusWith<UserHandle> AuthorizationManagerImpl::reacquireUser(OperationContext*
// Make a good faith effort to acquire an up-to-date user object, since the one
// we've cached is marked "out-of-date."
- auto swUserHandle = acquireUser(opCtx, UserRequest(userName, boost::none));
+ // TODO SERVER-72678 avoid this edge case hack when rearchitecting user acquisition. This is
+ // necessary now to preserve the mechanismData from the original UserRequest while eliminating
+ // the roles. If the roles aren't reset to none, it will cause LDAP acquisition to be bypassed
+ // in favor of reusing the ones from before.
+ UserRequest requestWithoutRoles(user->getUserRequest());
+ requestWithoutRoles.roles = boost::none;
+ auto swUserHandle = acquireUser(opCtx, requestWithoutRoles);
if (!swUserHandle.isOK()) {
return swUserHandle.getStatus();
}
diff --git a/src/mongo/db/auth/authorization_manager_impl.h b/src/mongo/db/auth/authorization_manager_impl.h
index db7fd95b70d..44acfa1def9 100644
--- a/src/mongo/db/auth/authorization_manager_impl.h
+++ b/src/mongo/db/auth/authorization_manager_impl.h
@@ -31,6 +31,7 @@
#include "mongo/db/auth/authorization_manager.h"
#include "mongo/db/auth/privilege.h"
+#include "mongo/db/auth/user.h"
#include "mongo/platform/atomic_word.h"
#include "mongo/platform/mutex.h"
#include "mongo/stdx/condition_variable.h"
diff --git a/src/mongo/db/auth/authorization_session_impl.cpp b/src/mongo/db/auth/authorization_session_impl.cpp
index a95df5f97ea..6c4bea361a3 100644
--- a/src/mongo/db/auth/authorization_session_impl.cpp
+++ b/src/mongo/db/auth/authorization_session_impl.cpp
@@ -728,11 +728,11 @@ void AuthorizationSessionImpl::_refreshUserInfoAsNeeded(OperationContext* opCtx)
auto swUser = getAuthorizationManager().reacquireUser(opCtx, currentUser);
if (!swUser.isOK()) {
auto& status = swUser.getStatus();
- // If an external user is no longer in the cache and cannot be acquired from the cache's
- // backing external service, it should be cleared from _authenticatedUser. This
- // guarantees that no operations can be performed until the external authorization
- // provider comes back up.
- if (name.getDB() == "$external"_sd) {
+ // If an LDAP user is no longer in the cache and cannot be acquired from the cache's
+ // backing LDAP host, it should be cleared from _authenticatedUser. This
+ // guarantees that no operations can be performed until the LDAP host comes back up.
+ // TODO SERVER-72678 avoid this edge case hack when rearchitecting user acquisition.
+ if (name.getDB() == "$external"_sd && currentUser->getUserRequest().mechanismData.empty()) {
clearUser();
LOGV2(5914804,
"Removed external user from session cache of user information because of "
@@ -761,6 +761,19 @@ void AuthorizationSessionImpl::_refreshUserInfoAsNeeded(OperationContext* opCtx)
"error"_attr = status);
return;
}
+ case ErrorCodes::ReauthenticationRequired: {
+ // An auth subsystem has indicated that client reauthentication is required. The
+ // session will exist in an expired state to signal this to drivers.
+ _expiredUserName = _authenticatedUser.value()->getName();
+ clearUser();
+ LOGV2(
+ 7119502,
+ "Removed user from session cache of user information because reauthentication "
+ "is required",
+ "user"_attr = name,
+ "error"_attr = status);
+ return;
+ }
default:
// Unrecognized error; assume that it's transient, and continue working with the
// out-of-date privilege data.
diff --git a/src/mongo/db/auth/builtin_roles.yml b/src/mongo/db/auth/builtin_roles.yml
index dfc1b718ca3..1ddd65b6d7c 100644
--- a/src/mongo/db/auth/builtin_roles.yml
+++ b/src/mongo/db/auth/builtin_roles.yml
@@ -304,6 +304,8 @@ roles:
- resync # clusterManager gets this also
- trafficRecord
- rotateCertificates
+ - oidcListKeys
+ - oidcRefreshKeys
- matchType: any_normal
actions: &hostManagerRoleDatabaseActions
- killCursors