Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e498b7e

Browse files
committedJul 11, 2024·
RUBY-3303 Add OIDC machine workflow auth
1 parent 3c5dc93 commit e498b7e

36 files changed

+1858
-378
lines changed
 

‎.evergreen/config.yml

+156-1
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,48 @@ functions:
454454
455455
CRYPT_SHARED_LIB_PATH="${CRYPT_SHARED_LIB_PATH}" SERVERLESS=1 SSL=ssl RVM_RUBY="${RVM_RUBY}" SINGLE_MONGOS="${SINGLE_MONGOS}" SERVERLESS_URI="${SERVERLESS_URI}" FLE="${FLE}" SERVERLESS_MONGODB_VERSION="${SERVERLESS_MONGODB_VERSION}" .evergreen/run-tests-serverless.sh
456456
457+
"run oidc vm tests":
458+
- command: subprocess.exec
459+
type: test
460+
params:
461+
working_dir: src
462+
binary: bash
463+
env:
464+
DRIVERS_TOOLS: ${DRIVERS_TOOLS}
465+
PROJECT_DIRECTORY: ${PROJECT_DIRECTORY}
466+
RVM_RUBY: ${RVM_RUBY}
467+
TEST_SCRIPT: ${TEST_SCRIPT}
468+
args:
469+
- .evergreen/${RUN_SCRIPT}
470+
471+
"run oidc prose tests":
472+
- command: subprocess.exec
473+
type: test
474+
params:
475+
working_dir: src
476+
binary: bash
477+
env:
478+
DRIVERS_TOOLS: ${DRIVERS_TOOLS}
479+
PROJECT_DIRECTORY: ${PROJECT_DIRECTORY}
480+
ENVIRONMENT: ${ENVIRONMENT}
481+
RVM_RUBY: ${RVM_RUBY}
482+
args:
483+
- .evergreen/run-tests-oidc-prose.sh
484+
485+
"run oidc unified tests":
486+
- command: subprocess.exec
487+
type: test
488+
params:
489+
working_dir: src
490+
binary: bash
491+
env:
492+
DRIVERS_TOOLS: ${DRIVERS_TOOLS}
493+
PROJECT_DIRECTORY: ${PROJECT_DIRECTORY}
494+
ENVIRONMENT: ${ENVIRONMENT}
495+
RVM_RUBY: ${RVM_RUBY}
496+
args:
497+
- .evergreen/run-tests-oidc-unified.sh
498+
457499
pre:
458500
- func: "fetch source"
459501
- func: "create expansions"
@@ -751,6 +793,77 @@ task_groups:
751793
tasks:
752794
- testazurekms-task
753795

796+
- name: test_oidc_task_group
797+
setup_group:
798+
- func: fetch source
799+
- func: create expansions
800+
- command: ec2.assume_role
801+
params:
802+
role_arn: ${aws_test_secrets_role}
803+
- command: subprocess.exec
804+
params:
805+
binary: bash
806+
include_expansions_in_env:
807+
- AWS_ACCESS_KEY_ID
808+
- AWS_SECRET_ACCESS_KEY
809+
- AWS_SESSION_TOKEN
810+
env:
811+
MONGODB_VERSION: '8.0'
812+
args:
813+
- ${DRIVERS_TOOLS}/.evergreen/auth_oidc/setup.sh
814+
setup_group_can_fail_task: true
815+
setup_group_timeout_secs: 1800
816+
tasks:
817+
- oidc-auth-test-latest
818+
819+
- name: test_oidc_azure_task_group
820+
setup_group:
821+
- func: fetch source
822+
- func: create expansions
823+
- command: shell.exec
824+
params:
825+
shell: bash
826+
script: |-
827+
set -o errexit
828+
${PREPARE_SHELL}
829+
export AZUREOIDC_VMNAME_PREFIX="RUBY_DRIVER"
830+
$DRIVERS_TOOLS/.evergreen/auth_oidc/azure/setup.sh
831+
teardown_task:
832+
- command: shell.exec
833+
params:
834+
shell: bash
835+
script: |-
836+
${PREPARE_SHELL}
837+
$DRIVERS_TOOLS/.evergreen/auth_oidc/azure/teardown.sh
838+
setup_group_can_fail_task: true
839+
setup_group_timeout_secs: 1800
840+
tasks:
841+
- oidc-auth-test-azure-latest
842+
843+
- name: test_oidc_gcp_task_group
844+
setup_group:
845+
- func: fetch source
846+
- func: create expansions
847+
- command: shell.exec
848+
params:
849+
shell: bash
850+
script: |-
851+
set -o errexit
852+
${PREPARE_SHELL}
853+
export GCPOIDC_VMNAME_PREFIX="RUBY_DRIVER"
854+
$DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/setup.sh
855+
teardown_task:
856+
- command: shell.exec
857+
params:
858+
shell: bash
859+
script: |-
860+
${PREPARE_SHELL}
861+
$DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/teardown.sh
862+
setup_group_can_fail_task: true
863+
setup_group_timeout_secs: 1800
864+
tasks:
865+
- oidc-auth-test-gcp-latest
866+
754867
tasks:
755868
- name: "test-atlas"
756869
commands:
@@ -895,8 +1008,37 @@ tasks:
8951008
LAMBDA_STACK_NAME: "dbx-ruby-lambda"
8961009
RVM_RUBY: ruby-3.2
8971010
MONGODB_URI: ${MONGODB_URI}
898-
axes:
8991011

1012+
- name: oidc-auth-test-latest
1013+
commands:
1014+
- func: "run oidc prose tests"
1015+
vars:
1016+
ENVIRONMENT: test
1017+
- func: "run oidc unified tests"
1018+
vars:
1019+
ENVIRONMENT: test
1020+
1021+
- name: oidc-auth-test-azure-latest
1022+
commands:
1023+
- func: "run oidc vm tests"
1024+
vars:
1025+
TEST_SCRIPT: run-tests-oidc-prose.sh
1026+
RUN_SCRIPT: run-tests-oidc-azure.sh
1027+
- func: "run oidc vm tests"
1028+
vars:
1029+
TEST_SCRIPT: run-tests-oidc-unified.sh
1030+
RUN_SCRIPT: run-tests-oidc-azure.sh
1031+
1032+
- name: oidc-auth-test-gcp-latest
1033+
commands:
1034+
- func: "run oidc prose tests"
1035+
vars:
1036+
ENVIRONMENT: gcp
1037+
- func: "run oidc unified tests"
1038+
vars:
1039+
ENVIRONMENT: gcp
1040+
1041+
axes:
9001042
- id: preload
9011043
display_name: Preload server
9021044
values:
@@ -1898,3 +2040,16 @@ buildvariants:
18982040
display_name: "AWS Lambda"
18992041
tasks:
19002042
- name: test_aws_lambda_task_group
2043+
2044+
- matrix_name: test-oidc-variant
2045+
matrix_spec:
2046+
ruby: "ruby-3.2"
2047+
fle: helper
2048+
topology: standalone
2049+
os: ubuntu2004
2050+
mongodb-version: latest
2051+
display_name: "OIDC auth tests: latest ruby-3.2"
2052+
tasks:
2053+
- test_oidc_task_group
2054+
- test_oidc_azure_task_group
2055+
- test_oidc_gcp_task_group

‎.evergreen/run-tests-oidc-azure.sh

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
set -o xtrace # Write all commands first to stderr
3+
set -o errexit # Exit the script with error if any of the commands fail
4+
5+
export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-ruby-driver.tgz
6+
tar czf $AZUREOIDC_DRIVERS_TAR_FILE .
7+
export AZUREOIDC_TEST_CMD="source ./env.sh && ENVIRONMENT=azure ./.evergreen/${TEST_SCRIPT}"
8+
export PROJECT_DIRECTORY=$PROJECT_DIRECTORY
9+
bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh

‎.evergreen/run-tests-oidc-gcp.sh

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
set -o xtrace # Write all commands first to stderr
3+
set -o errexit # Exit the script with error if any of the commands fail
4+
5+
export GCPOIDC_DRIVERS_TAR_FILE=/tmp/mongo-ruby-driver.tgz
6+
tar czf $GCPOIDC_DRIVERS_TAR_FILE .
7+
export GCPOIDC_TEST_CMD="source ./secrets-export.sh drivers/gcpoidc && ENVIRONMENT=gcp ./.evergreen/${TEST_SCRIPT}"
8+
export PROJECT_DIRECTORY=$PROJECT_DIRECTORY
9+
bash $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/run-driver-test.sh

‎.evergreen/run-tests-oidc-prose.sh

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/bin/bash
2+
3+
set -ex
4+
5+
ENVIRONMENT=${ENVIRONMENT:-"test"}
6+
7+
. `dirname "$0"`/../spec/shared/shlib/distro.sh
8+
. `dirname "$0"`/../spec/shared/shlib/set_env.sh
9+
. `dirname "$0"`/functions.sh
10+
11+
set_env_vars
12+
set_env_python
13+
set_env_ruby
14+
15+
bundle_install
16+
bundle exec rspec -fd spec/integration/oidc/${ENVIRONMENT}_machine_auth_flow_prose_spec.rb
17+
18+
test_status=$?
19+
20+
kill_jruby
21+
22+
exit ${test_status}

‎.evergreen/run-tests-oidc-unified.sh

Whitespace-only changes.

‎.mod/drivers-evergreen-tools

‎lib/mongo/auth.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
require 'mongo/auth/cr'
2828
require 'mongo/auth/gssapi'
2929
require 'mongo/auth/ldap'
30+
require 'mongo/auth/oidc'
3031
require 'mongo/auth/scram'
3132
require 'mongo/auth/scram256'
3233
require 'mongo/auth/x509'
@@ -70,6 +71,7 @@ module Auth
7071
aws: Aws,
7172
gssapi: Gssapi,
7273
mongodb_cr: CR,
74+
mongodb_oidc: Oidc,
7375
mongodb_x509: X509,
7476
plain: LDAP,
7577
scram: Scram,
@@ -89,7 +91,7 @@ module Auth
8991
# value of speculativeAuthenticate field of hello response of
9092
# the handshake on the specified connection.
9193
#
92-
# @return [ Auth::Aws | Auth::CR | Auth::Gssapi | Auth::LDAP |
94+
# @return [ Auth::Aws | Auth::CR | Auth::Gssapi | Auth::LDAP | Auth::Oidc
9395
# Auth::Scram | Auth::Scram256 | Auth::X509 ] The authenticator.
9496
#
9597
# @since 2.0.0

‎lib/mongo/auth/oidc.rb

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
# Copyright (C) 2014-2024 MongoDB, Inc.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
module Mongo
19+
module Auth
20+
21+
# Defines behavior for OIDC authentication.
22+
#
23+
# @api private
24+
class Oidc < Base
25+
attr_reader :speculative_auth_result
26+
27+
# The authentication mechanism string.
28+
#
29+
# @since 2.20.0
30+
MECHANISM = 'MONGODB-OIDC'.freeze
31+
32+
# Initializes the OIDC authenticator.
33+
#
34+
# @param [ Auth::User ] user The user to authenticate.
35+
# @param [ Mongo::Connection ] connection The connection to authenticate over.
36+
#
37+
# @option opts [ BSON::Document | nil ] speculative_auth_result The
38+
# value of speculativeAuthenticate field of hello response of
39+
# the handshake on the specified connection.
40+
def initialize(user, connection, **opts)
41+
super
42+
@speculative_auth_result = opts[:speculative_auth_result]
43+
@machine_workflow = MachineWorkflow::new(auth_mech_properties: user.auth_mech_properties)
44+
end
45+
46+
# Log the user in on the current connection.
47+
#
48+
# @return [ BSON::Document ] The document of the authentication response.
49+
def login
50+
execute_workflow(connection: connection, conversation: conversation)
51+
end
52+
53+
private
54+
55+
def execute_workflow(connection:, conversation:)
56+
# If there is a cached access token, try to authenticate with it. If
57+
# authentication fails with an Authentication error (18),
58+
# invalidate the access token, fetch a new access token, and try
59+
# to authenticate again.
60+
# If the server fails for any other reason, do not clear the cache.
61+
if cache.access_token?
62+
token = cache.access_token
63+
msg = conversation.start(connection: connection, token: token)
64+
begin
65+
dispatch_msg(connection, conversation, msg)
66+
rescue AuthError => error
67+
cache.invalidate(token: token)
68+
execute_workflow(connection: connection, conversation: conversation)
69+
end
70+
end
71+
# This is the normal flow when no token is in the cache. Execute the
72+
# machine callback to get the token, put it in the caches, and then
73+
# send the saslStart to the server.
74+
token = machine_workflow.execute
75+
cache.access_token = token
76+
connection.access_token = token
77+
msg = conversation.start(connection: connection, token: token)
78+
dispatch_msg(connection, conversation, msg)
79+
end
80+
end
81+
end
82+
end
83+
84+
require 'mongo/auth/oidc/conversation'
85+
require 'mongo/auth/oidc/machine_workflow'
86+
require 'mongo/auth/oidc/token_cache'

‎lib/mongo/auth/oidc/conversation.rb

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
# Copyright (C) 2024 MongoDB Inc.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
module Mongo
19+
module Auth
20+
class Oidc
21+
# Defines behaviour around a single OIDC conversation between the
22+
# client and the server.
23+
#
24+
# @api private
25+
class Conversation < ConversationBase
26+
# The base client message.
27+
START_MESSAGE = { saslStart: 1, mechanism: Oidc::MECHANISM }.freeze
28+
29+
# Create the new conversation.
30+
#
31+
# @example Create the new conversation.
32+
# Conversation.new(user, 'test.example.com')
33+
#
34+
# @param [ Auth::User ] user The user to converse about.
35+
# @param [ Mongo::Connection ] connection The connection to
36+
# authenticate over.
37+
#
38+
# @since 2.20.0
39+
def initialize(user, connection, **opts)
40+
super
41+
end
42+
43+
# OIDC machine workflow is always a saslStart with the payload being
44+
# the serialized jwt token.
45+
#
46+
# @param [ String ] token The access token.
47+
#
48+
# @return [ Hash ] The start document.
49+
def client_start_document(token:)
50+
START_MESSAGE.merge(payload: finish_payload(token: token))
51+
end
52+
53+
# Gets the serialized jwt payload for the token.
54+
#
55+
# @param [ String ] token The access token.
56+
#
57+
# @return [ BSON::Binary ] The serialized payload.
58+
def finish_payload(token:)
59+
payload = { jwt: token }.to_bson.to_s
60+
BSON::Binary.new(payload)
61+
end
62+
63+
# Start the OIDC conversation. This returns the first message that
64+
# needs to be sent to the server.
65+
#
66+
# @param [ Server::Connection ] connection The connection being authenticated.
67+
#
68+
# @return [ Protocol::Message ] The first OIDC conversation message.
69+
def start(connection:, token:)
70+
selector = client_start_document(token: token)
71+
build_message(connection, '$external', selector)
72+
end
73+
end
74+
end
75+
end
76+
end
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
# Copyright (C) 2024 MongoDB Inc.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
module Mongo
19+
module Auth
20+
class Oidc
21+
# The machine callback workflow is a 1 step execution of the callback
22+
# to get an OIDC token to connect with.
23+
class MachineWorkflow
24+
attr_reader :callback, :callback_lock, :last_executed
25+
26+
# The number of milliseconds to throttle the callback execution.
27+
THROTTLE_MS = 100
28+
# The default timeout for callback execution.
29+
TIMEOUT_MS = 60000
30+
# The current OIDC version.
31+
OIDC_VERSION = 1
32+
33+
def initialize(auth_mech_properties: {})
34+
@callback = CallbackFactory.get_callback(auth_mech_properties: auth_mech_properties)
35+
@callback_lock = Mutex.new
36+
# Ensure the first execution happens immediately.
37+
@last_executed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - THROTTLE_MS - 1
38+
end
39+
40+
# Execute the machine callback.
41+
def execute
42+
# Aquire lock before executing the callback and throttle calling it
43+
# to every 100ms.
44+
callback_lock.synchronize do
45+
difference = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - last_executed
46+
if difference <= THROTTLE_MS
47+
sleep(difference)
48+
end
49+
@last_executed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
50+
callback.execute(params: { timeout: TIMEOUT_MS, version: OIDC_VERSION })
51+
end
52+
end
53+
end
54+
end
55+
end
56+
end
57+
58+
require 'mongo/auth/oidc/machine_workflow/azure_callback'
59+
require 'mongo/auth/oidc/machine_workflow/gcp_callback'
60+
require 'mongo/auth/oidc/machine_workflow/test_callback'
61+
require 'mongo/auth/oidc/machine_workflow/callback_factory'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
# Copyright (C) 2024 MongoDB Inc.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
module Mongo
19+
module Auth
20+
class Oidc
21+
class MachineWorkflow
22+
class AzureCallback
23+
# The base Azure endpoint
24+
AZURE_BASE_URI = 'http://169.254.169.254/metadata/identity/oauth2/token'
25+
# The Azure headers.
26+
AZURE_HEADERS = { Metadata: 'true', Accept: 'application/json' }.freeze
27+
28+
attr_reader :token_resource, :username
29+
30+
def initialize(auth_mech_properties: {})
31+
@token_resource = auth_mech_properties[:token_resource]
32+
end
33+
34+
# Hits the Azure endpoint in order to get the token.
35+
def execute(params: {})
36+
query = { resource: token_resource, 'api-version' => '2018-02-01' }
37+
if username
38+
query[:client_id] = username
39+
end
40+
uri = URI(AZURE_BASE_URI);
41+
uri.query = ::URI.encode_www_form(query)
42+
request = Net::HTTP::Get.new(uri, AZURE_HEADERS)
43+
response = Timeout.timeout(params[:timeout]) do
44+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: false) do |http|
45+
http.request(request)
46+
end
47+
end
48+
if response.code != '200'
49+
raise Error::OidcError,
50+
"Azure metadata host responded with code #{response.code}"
51+
end
52+
result = JSON.parse(response.body)
53+
{ access_token: result['access_token'], expires_in: result['expires_in'] }
54+
end
55+
end
56+
end
57+
end
58+
end
59+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
# Copyright (C) 2024 MongoDB Inc.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
module Mongo
19+
module Auth
20+
class Oidc
21+
class MachineWorkflow
22+
module CallbackFactory
23+
# Map of environment name to the workflow callbacks.
24+
CALLBACKS = {
25+
'azure' => AzureCallback,
26+
'gcp' => GcpCallback,
27+
'test' => TestCallback
28+
}
29+
30+
# Gets the callback based on the auth mechanism properties.
31+
module_function def get_callback(auth_mech_properties: {})
32+
if auth_mech_properties[:oidc_callback]
33+
auth_mech_properties[:oidc_callback]
34+
else
35+
callback = CALLBACKS[auth_mech_properties[:environment]]
36+
if !callback
37+
raise Error::OidcError, "No OIDC machine callback found for environment: #{auth_mech_properties[:environment]}"
38+
end
39+
callback.new(auth_mech_properties: auth_mech_properties)
40+
end
41+
end
42+
end
43+
end
44+
end
45+
end
46+
end
47+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
# Copyright (C) 2024 MongoDB Inc.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
module Mongo
19+
module Auth
20+
class Oidc
21+
class MachineWorkflow
22+
class GcpCallback
23+
# The base GCP endpoint
24+
GCP_BASE_URI = 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity'.freeze
25+
# The GCP headers.
26+
GCP_HEADERS = { 'Metadata-Flavor': 'Google' }.freeze
27+
28+
attr_reader :token_resource
29+
30+
# Initialize the Gcp callback.
31+
#
32+
# @params [ Hash ] auth_mech_properties The auth mech properties.
33+
def initialize(auth_mech_properties: {})
34+
@token_resource = auth_mech_properties[:token_resource]
35+
end
36+
37+
# Hits the GCP endpoint in order to get the token. The token_resource will
38+
# become the audience parameter in the URI.
39+
#
40+
# @returns [ Hash ] A hash with the access token.
41+
def execute(params: {})
42+
uri = URI(GCP_BASE_URI);
43+
uri.query = ::URI.encode_www_form({ audience: token_resource })
44+
request = Net::HTTP::Get.new(uri, GCP_HEADERS)
45+
response = Timeout.timeout(params[:timeout]) do
46+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: false) do |http|
47+
http.request(request)
48+
end
49+
end
50+
if response.code != '200'
51+
raise Error::OidcError,
52+
"GCP metadata host responded with code #{response.code}"
53+
end
54+
{ access_token: JSON.parse(response.body) }
55+
end
56+
end
57+
end
58+
end
59+
end
60+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
# Copyright (C) 2024 MongoDB Inc.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
module Mongo
19+
module Auth
20+
class Oidc
21+
class MachineWorkflow
22+
class TestCallback
23+
# We don't need to do anything with the auth mech properties
24+
# passed in here.
25+
def initialize(auth_mech_properties: {})
26+
end
27+
28+
# Loads the token from the filesystem based on the OIDC_TOKEN_FILE
29+
# environment variable.
30+
#
31+
# @params [ Hash ] params timeout The timeout before cancelling.
32+
#
33+
# @returns [ Hash ] The access token.
34+
def execute(params: {})
35+
Timeout.timeout(params[:timeout]) do
36+
location = ENV.fetch('OIDC_TOKEN_FILE')
37+
token = File.read(location)
38+
{ access_token: token }
39+
end
40+
end
41+
end
42+
end
43+
end
44+
end
45+
end

‎lib/mongo/auth/oidc/token_cache.rb

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
# Copyright (C) 2024 MongoDB Inc.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
module Mongo
19+
module Auth
20+
class Oidc
21+
# Represents a cache of the OIDC access token.
22+
class TokenCache
23+
attr_accessor :access_token
24+
attr_reader :lock
25+
26+
def initialize
27+
@lock = Mutex.new
28+
end
29+
30+
# Is there an access token present in the cache?
31+
#
32+
# @returns [ Boolean ] True if present, false if not.
33+
def access_token?
34+
!!@access_token
35+
end
36+
37+
# Invalidate the token. Will only invalidate if the token
38+
# matches the existing one and only one thread at a time
39+
# may invalidate the token.
40+
#
41+
# @params [ String ] token The access token to invalidate.
42+
def invalidate(token:)
43+
lock.synchronize do
44+
if (access_token == token)
45+
@access_token = nil
46+
end
47+
end
48+
end
49+
end
50+
end
51+
end
52+
end

‎lib/mongo/auth/user.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ def spec
212212
# @api private
213213
def self.default_auth_source(options)
214214
case options[:auth_mech]
215-
when :aws, :gssapi, :mongodb_x509
215+
when :aws, :gssapi, :mongodb_x509, :mongodb_oidc
216216
'$external'
217217
when :plain
218218
options[:database] || '$external'

‎lib/mongo/client.rb

+36-6
Original file line numberDiff line numberDiff line change
@@ -1515,23 +1515,53 @@ def validate_authentication_options!
15151515
raise Mongo::Auth::InvalidMechanism.new(auth_mech)
15161516
end
15171517

1518-
if user.nil? && !%i(aws mongodb_x509).include?(auth_mech)
1518+
if user.nil? && !%i(aws mongodb_x509 mongodb_oidc).include?(auth_mech)
15191519
raise Mongo::Auth::InvalidConfiguration, "Username is required for auth mechanism #{auth_mech}"
15201520
end
15211521

1522-
if password.nil? && !%i(aws gssapi mongodb_x509).include?(auth_mech)
1522+
if password.nil? && !%i(aws gssapi mongodb_oidc mongodb_x509).include?(auth_mech)
15231523
raise Mongo::Auth::InvalidConfiguration, "Password is required for auth mechanism #{auth_mech}"
15241524
end
15251525

1526-
if password && auth_mech == :mongodb_x509
1527-
raise Mongo::Auth::InvalidConfiguration, 'Password is not supported for :mongodb_x509 auth mechanism'
1526+
if password && (auth_mech == :mongodb_x509 || auth_mech == :mongodb_oidc)
1527+
raise Mongo::Auth::InvalidConfiguration, "Password is not supported for #{auth_mech} auth mechanism"
15281528
end
15291529

15301530
if auth_mech == :aws && user && !password
15311531
raise Mongo::Auth::InvalidConfiguration, 'Username is provided but password is not provided for :aws auth mechanism'
15321532
end
15331533

1534-
if %i(aws gssapi mongodb_x509).include?(auth_mech)
1534+
if auth_mech == :mongodb_oidc
1535+
if mech_properties
1536+
if mech_properties[:environment].nil? && mech_properties[:oidc_callback].nil?
1537+
raise Mongo::Auth::InvalidConfiguration, 'An OIDC callback or environment must be provided in the auth mechanism properties for OIDC authentication.'
1538+
end
1539+
1540+
if %w(azure gcp).include?(mech_properties[:environment]) && mech_properties[:token_resource].nil?
1541+
raise Mongo::Auth::InvalidConfiguration, "Token resource is required when using OIDC machine workflow: #{mech_properties[:environment]}"
1542+
end
1543+
1544+
mech_properties.each_pair do |property, value|
1545+
if !%w(oidc_callback environment token_resource).include?(property)
1546+
raise Mongo::Auth::InvalidConfiguration, "Auth mechanism property #{property} is not supported with OIDC authentication."
1547+
end
1548+
1549+
if property === 'environment'
1550+
if !%w(azure gcp test).include?(value)
1551+
raise Mongo::Auth::InvalidConfiguration, "Auth mechanism property #{property} must be one of azure or gcp, got #{value}"
1552+
end
1553+
1554+
if value === 'test' && user
1555+
raise Mongo::Auth::InvalidConfiguration, 'Username cannot be provided for test machine workflow with OIDC authentication.'
1556+
end
1557+
end
1558+
end
1559+
else
1560+
raise Mongo::Auth::InvalidConfiguration, 'An OIDC callback or environment must be provided in the auth mechanism properties for OIDC authentication.'
1561+
end
1562+
end
1563+
1564+
if %i(aws gssapi mongodb_x509 mongodb_oidc).include?(auth_mech)
15351565
if !['$external', nil].include?(auth_source)
15361566
raise Mongo::Auth::InvalidConfiguration, "#{auth_source} is an invalid auth source for #{auth_mech}; valid options are $external and nil"
15371567
end
@@ -1542,7 +1572,7 @@ def validate_authentication_options!
15421572
end
15431573
end
15441574

1545-
if mech_properties && !%i(aws gssapi).include?(auth_mech)
1575+
if mech_properties && !%i(aws gssapi mongodb_oidc).include?(auth_mech)
15461576
raise Mongo::Auth::InvalidConfiguration, ":mechanism_properties are not supported for auth mechanism #{auth_mech}"
15471577
end
15481578
end

‎lib/mongo/error.rb

+1
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ def write_concern_error_labels
190190
require 'mongo/error/no_service_connection_available'
191191
require 'mongo/error/no_server_available'
192192
require 'mongo/error/no_srv_records'
193+
require 'mongo/error/oidc_error'
193194
require 'mongo/error/session_ended'
194195
require 'mongo/error/sessions_not_supported'
195196
require 'mongo/error/session_not_materialized'

‎lib/mongo/error/oidc_error.rb

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
# Copyright (C) 2024 MongoDB Inc.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
module Mongo
19+
class Error
20+
class OidcError < Error
21+
end
22+
end
23+
end

‎lib/mongo/uri.rb

+1
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ class URI
199199
# MONGODB-CR is deprecated and will be removed in driver version 3.0
200200
'MONGODB-CR' => :mongodb_cr,
201201
'MONGODB-X509' => :mongodb_x509,
202+
'MONGODB-OIDC' => :mongodb_oidc,
202203
'PLAIN' => :plain,
203204
'SCRAM-SHA-1' => :scram,
204205
'SCRAM-SHA-256' => :scram256,

‎lib/mongo/uri/options_mapper.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -848,7 +848,9 @@ def stringify_zlib_compression_level(value)
848848
def hash_extractor(name, value)
849849
h = {}
850850
value.split(',').each do |tag|
851-
k, v = tag.split(':')
851+
# Auth mech properties can have a : in them with the introduction of OIDC,
852+
# so e ensure to split only into 2 strings.
853+
k, v = tag.split(':', 2)
852854
if v.nil?
853855
log_warn("Invalid hash value for #{name}: key `#{k}` does not have a value: #{value}")
854856
next
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
require 'spec_helper'
5+
6+
describe 'OIDC Authentication Prose Tests' do
7+
require_oidc 'azure'
8+
9+
describe 'Azure Machine Authentication Flow Prose Tests' do
10+
# 5.1 Azure With No Username
11+
context 'when no username is provided' do
12+
let(:client) do
13+
Mongo::Client.new(ENV.fetch('MONGODB_URI_SINGLE'),
14+
database: 'test',
15+
auth_mech_properties: {
16+
TOKEN_RESOURCE: ENV.fetch('AZUREOIDC_RESOURCE')
17+
}
18+
)
19+
end
20+
21+
let(:collection) do
22+
client['test']
23+
end
24+
25+
after(:each) do
26+
client.close
27+
end
28+
29+
it 'successfully authenticates' do
30+
expect(collection.find_one).to be_nil
31+
end
32+
end
33+
34+
# 5.2 Azure With Bad Username
35+
context 'when a bad username is provided' do
36+
let(:client) do
37+
Mongo::Client.new(ENV.fetch('MONGODB_URI_SINGLE'),
38+
database: 'test',
39+
user: 'bad',
40+
auth_mech_properties: {
41+
TOKEN_RESOURCE: ENV.fetch('AZUREOIDC_RESOURCE')
42+
}
43+
)
44+
end
45+
46+
let(:collection) do
47+
client['test']
48+
end
49+
50+
after(:each) do
51+
client.close
52+
end
53+
54+
it 'fails authentication' do
55+
expect {
56+
collection.find_one
57+
}.to raise_error(OidcError)
58+
end
59+
end
60+
61+
# No prose test in spec for this
62+
context 'when a valid username is provided' do
63+
let(:client) do
64+
Mongo::Client.new(ENV.fetch('MONGODB_URI_SINGLE'),
65+
database: 'test',
66+
user: ENV.fetch('AZUREOIDC_USERNAME'),
67+
auth_mech_properties: {
68+
TOKEN_RESOURCE: ENV.fetch('AZUREOIDC_RESOURCE')
69+
}
70+
)
71+
end
72+
73+
let(:collection) do
74+
client['test']
75+
end
76+
77+
after(:each) do
78+
client.close
79+
end
80+
81+
it 'successfully authenticates' do
82+
expect(collection.find_one).to be_nil
83+
end
84+
end
85+
end
86+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
require 'spec_helper'
5+
6+
describe 'OIDC Authentication Prose Tests' do
7+
require_oidc 'gcp'
8+
9+
# No prose tests in the spec for GCP, testing the two cases.
10+
describe 'GCP Machine Authentication Flow Prose Tests' do
11+
context 'when the token resource is valid' do
12+
let(:client) do
13+
Mongo::Client.new(ENV.fetch('MONGODB_URI_SINGLE'),
14+
database: 'test',
15+
auth_mech_properties: {
16+
TOKEN_RESOURCE: ENV.fetch('GCPOIDC_AUDIENCE')
17+
}
18+
)
19+
end
20+
21+
let(:collection) do
22+
client['test']
23+
end
24+
25+
after(:each) do
26+
client.close
27+
end
28+
29+
it 'successfully authenticates' do
30+
expect(collection.find_one).to be_nil
31+
end
32+
end
33+
34+
context 'when the token resource is invalid' do
35+
let(:client) do
36+
Mongo::Client.new(ENV.fetch('MONGODB_URI_SINGLE'),
37+
database: 'test',
38+
auth_mech_properties: {
39+
TOKEN_RESOURCE: 'bad'
40+
}
41+
)
42+
end
43+
44+
let(:collection) do
45+
client['test']
46+
end
47+
48+
after(:each) do
49+
client.close
50+
end
51+
52+
it 'fails authentication' do
53+
expect {
54+
collection.find_one
55+
}.to raise_error(OidcError)
56+
end
57+
end
58+
end
59+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
require 'spec_helper'
5+
6+
describe 'OIDC Authentication Prose Tests' do
7+
require_oidc 'test'
8+
9+
describe 'Machine Authentication Flow Prose Tests' do
10+
context 'when using callback authentication' do
11+
# 1.1 Callback is called during authentication
12+
context 'when executing an operation' do
13+
end
14+
15+
# 1.2 Callback is called once for multiple connections
16+
context 'when using multiple connections' do
17+
end
18+
end
19+
20+
context 'when validating callbacks' do
21+
# 2.1 Valid Callback Inputs
22+
context 'when callback inputs are valie' do
23+
end
24+
25+
# 2.2 OIDC Callback Returns Null
26+
context 'when the callback returns null' do
27+
end
28+
29+
# 2.3 OIDC Callback Returns Missing Data
30+
context 'when the callback returns missing data' do
31+
end
32+
33+
# 2.4 Invalid Client Configuration with Callback
34+
context 'when the client is misconfigured' do
35+
end
36+
37+
# 2.5 Invalid use of ALLOWED_HOSTS
38+
context 'when allowed hosts are misconfigured' do
39+
end
40+
end
41+
42+
context 'when authentication fails' do
43+
# 3.1 Authentication failure with cached tokens fetch a new token and retry auth
44+
context 'when tokens are cached' do
45+
end
46+
47+
# 3.2 Authentication failures without cached tokens return an error
48+
context 'when no tokens are cached' do
49+
end
50+
51+
# 3.3 Unexpected error code does not clear the cache
52+
context 'when error code is unexpected' do
53+
end
54+
end
55+
56+
context 'when reauthenticating' do
57+
# 4.1 Reauthentication Succeeds
58+
context 'when reauthentication succeeds' do
59+
end
60+
61+
context 'when reauthentication fails' do
62+
# 4.2 Read Commands Fail If Reauthentication Fails
63+
context 'when executing a read' do
64+
end
65+
66+
# 4.3 Write Commands Fail If Reauthentication Fails
67+
context 'when executing a write' do
68+
end
69+
end
70+
end
71+
end
72+
end

‎spec/lite_spec_helper.rb

+7
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ def require_atlas
154154
end
155155
end
156156

157+
def require_oidc(environment)
158+
env = ENV['ENVIRONMENT']
159+
before do
160+
skip "Set ENVIRONMENT in the environment to #{environment} to run the OIDC #{environment} tests" if env != environment
161+
end
162+
end
163+
157164
if SpecConfig.instance.ci?
158165
SdamFormatterIntegration.subscribe
159166
config.add_formatter(JsonExtFormatter, File.join(File.dirname(__FILE__), '../tmp/rspec.json'))

‎spec/mongo/auth/invalid_mechanism_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
let(:exception) { described_class.new(:foo) }
99

1010
it 'includes all built in mechanisms' do
11-
expect(exception.message).to eq(':foo is invalid, please use one of the following mechanisms: :aws, :gssapi, :mongodb_cr, :mongodb_x509, :plain, :scram, :scram256')
11+
expect(exception.message).to eq(':foo is invalid, please use one of the following mechanisms: :aws, :gssapi, :mongodb_cr, :mongodb_oidc, :mongodb_x509, :plain, :scram, :scram256')
1212
end
1313
end
1414
end
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
require 'spec_helper'
5+
6+
describe Mongo::Auth::Oidc::Conversation do
7+
let(:user) do
8+
Mongo::Auth::User.new(user: 'test')
9+
end
10+
11+
let(:connection) do
12+
double('connection')
13+
end
14+
15+
let(:conversation) do
16+
described_class.new(user, connection)
17+
end
18+
19+
let(:features) do
20+
double('features')
21+
end
22+
23+
describe '#start' do
24+
before do
25+
expect(connection).to receive(:features).and_return(features)
26+
expect(connection).to receive(:mongos?).and_return(false)
27+
expect(features).to receive(:op_msg_enabled?).and_return(true)
28+
end
29+
30+
let(:token) do
31+
'token'
32+
end
33+
34+
let(:msg) do
35+
conversation.start(connection: connection, token: token)
36+
end
37+
38+
let(:selector) do
39+
msg.instance_variable_get(:@main_document)
40+
end
41+
42+
it 'sets the sasl start flag' do
43+
expect(selector[:saslStart]).to eq(1)
44+
end
45+
46+
it 'sets the mechanism' do
47+
expect(selector[:mechanism]).to eq('MONGODB-OIDC')
48+
end
49+
50+
it 'sets the payload' do
51+
expect(selector[:payload].data).to eq({ jwt: token }.to_bson.to_s)
52+
end
53+
end
54+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
require 'spec_helper'
5+
6+
describe Mongo::Auth::Oidc::MachineWorkflow::AzureCallback do
7+
8+
let(:properties) do
9+
{ token_resource: 'audience' }
10+
end
11+
12+
let(:callback) do
13+
described_class.new(auth_mech_properties: properties)
14+
end
15+
16+
describe '#execute' do
17+
context 'when the response is a 200' do
18+
let(:response) do
19+
double('response')
20+
end
21+
22+
before do
23+
expect(response).to receive(:code).and_return('200')
24+
expect(response).to receive(:body).and_return('{ "access_token": "token", "expires_in": 500 }')
25+
allow(Net::HTTP).to receive(:start).with('169.254.169.254', 80, use_ssl: false).and_return(response)
26+
end
27+
28+
let(:result) do
29+
callback.execute(params: { timeout: 50 })
30+
end
31+
32+
it 'returns the token' do
33+
expect(result[:access_token]).to eq('token')
34+
end
35+
end
36+
37+
context 'when the response is not a 200' do
38+
let(:response) do
39+
double('response')
40+
end
41+
42+
before do
43+
expect(response).to receive(:code).twice.and_return('500')
44+
allow(Net::HTTP).to receive(:start).with('169.254.169.254', 80, use_ssl: false).and_return(response)
45+
end
46+
47+
let(:result) do
48+
end
49+
50+
it 'raises an error' do
51+
expect {
52+
callback.execute(params: { timeout: 50 })
53+
}.to raise_error(Mongo::Error::OidcError)
54+
end
55+
end
56+
end
57+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
require 'spec_helper'
5+
6+
describe Mongo::Auth::Oidc::MachineWorkflow::CallbackFactory do
7+
describe '.get_callback' do
8+
context 'when an OIDC_CALLBACK auth mech property is provided' do
9+
class OidcCallback
10+
def execute(params: {})
11+
{ access_token: 'test' }
12+
end
13+
end
14+
15+
let(:callback) do
16+
described_class.get_callback(auth_mech_properties: { oidc_callback: OidcCallback.new })
17+
end
18+
19+
it 'returns the user provided callback' do
20+
expect(callback).to be_a OidcCallback
21+
end
22+
end
23+
24+
context 'when an environment auth mech property is provided' do
25+
context 'when the value is azure' do
26+
let(:callback) do
27+
described_class.get_callback(auth_mech_properties: { environment: 'azure', token_resource: 'resource' })
28+
end
29+
30+
it 'returns the azure callback' do
31+
expect(callback).to be_a Mongo::Auth::Oidc::MachineWorkflow::AzureCallback
32+
end
33+
end
34+
35+
context 'when the valie is gcp' do
36+
let(:callback) do
37+
described_class.get_callback(auth_mech_properties: { environment: 'gcp', token_resource: 'client_id' })
38+
end
39+
40+
it 'returns the gcp callback' do
41+
expect(callback).to be_a Mongo::Auth::Oidc::MachineWorkflow::GcpCallback
42+
end
43+
end
44+
45+
context 'when the value is test' do
46+
let(:callback) do
47+
described_class.get_callback(auth_mech_properties: { environment: 'test' })
48+
end
49+
50+
it 'returns the test callback' do
51+
expect(callback).to be_a Mongo::Auth::Oidc::MachineWorkflow::TestCallback
52+
end
53+
end
54+
55+
context 'when the value is unknown' do
56+
it 'raises an oidc error' do
57+
expect {
58+
described_class.get_callback(auth_mech_properties: { environment: 'nothing' })
59+
}.to raise_error(Mongo::Error::OidcError)
60+
end
61+
end
62+
end
63+
end
64+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
require 'spec_helper'
5+
6+
describe Mongo::Auth::Oidc::MachineWorkflow::GcpCallback do
7+
let(:properties) do
8+
{ token_resource: 'audience' }
9+
end
10+
11+
let(:callback) do
12+
described_class.new(auth_mech_properties: properties)
13+
end
14+
15+
describe '#execute' do
16+
context 'when the response is a 200' do
17+
let(:response) do
18+
double('response')
19+
end
20+
21+
before do
22+
expect(response).to receive(:code).and_return('200')
23+
expect(response).to receive(:body).and_return('"token"')
24+
allow(Net::HTTP).to receive(:start).with('metadata', 80, use_ssl: false).and_return(response)
25+
end
26+
27+
let(:result) do
28+
callback.execute(params: { timeout: 50 })
29+
end
30+
31+
it 'returns the token' do
32+
expect(result[:access_token]).to eq('token')
33+
end
34+
end
35+
36+
context 'when the response is not a 200' do
37+
let(:response) do
38+
double('response')
39+
end
40+
41+
before do
42+
expect(response).to receive(:code).twice.and_return('500')
43+
allow(Net::HTTP).to receive(:start).with('metadata', 80, use_ssl: false).and_return(response)
44+
end
45+
46+
let(:result) do
47+
end
48+
49+
it 'raises an error' do
50+
expect {
51+
callback.execute(params: { timeout: 50 })
52+
}.to raise_error(Mongo::Error::OidcError)
53+
end
54+
end
55+
end
56+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
require 'spec_helper'
5+
6+
describe Mongo::Auth::Oidc::MachineWorkflow::TestCallback do
7+
let(:callback) do
8+
described_class.new()
9+
end
10+
11+
describe '#execute' do
12+
context 'when a token path is provided' do
13+
let(:path) do
14+
'/path/to/file'
15+
end
16+
17+
before do
18+
allow(ENV).to receive(:fetch).with('OIDC_TOKEN_FILE').and_return(path)
19+
allow(File).to receive(:read).with(path).and_return('token')
20+
end
21+
22+
let(:result) do
23+
callback.execute(params: { timeout: 50 })
24+
end
25+
26+
it 'returns the token' do
27+
expect(result[:access_token]).to eq('token')
28+
end
29+
end
30+
31+
context 'when a token path is not provided' do
32+
it 'raises an error' do
33+
expect {
34+
callback.execute(params: { timeout: 50 })
35+
}.to raise_error(KeyError)
36+
end
37+
end
38+
end
39+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
require 'spec_helper'
5+
6+
describe Mongo::Auth::Oidc::MachineWorkflow do
7+
let(:callback) do
8+
double('callback')
9+
end
10+
11+
let(:properties) do
12+
{ oidc_callback: callback }
13+
end
14+
15+
describe '#start' do
16+
context 'when executing for the first time' do
17+
let(:workflow) do
18+
described_class.new(auth_mech_properties: properties)
19+
end
20+
21+
let(:token) do
22+
'token'
23+
end
24+
25+
let(:params) do
26+
{ timeout: 60000, version: 1 }
27+
end
28+
29+
before do
30+
expect(callback).to receive(:execute).with(params: params).and_return({ access_token: token })
31+
end
32+
33+
let(:result) do
34+
workflow.execute
35+
end
36+
37+
it 'returns the token result' do
38+
expect(result).to eq({ access_token: token })
39+
end
40+
end
41+
42+
context 'when executing multiple times in succession' do
43+
let!(:workflow) do
44+
described_class.new(auth_mech_properties: properties)
45+
end
46+
47+
let(:token) do
48+
'token'
49+
end
50+
51+
let!(:time) do
52+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
53+
end
54+
55+
let(:params) do
56+
{ timeout: 60000, version: 1 }
57+
end
58+
59+
before do
60+
expect(callback).to receive(:execute).exactly(10).times.and_return({ access_token: token })
61+
end
62+
63+
it 'throttles the execution at 100ms' do
64+
10.times do
65+
workflow.execute
66+
end
67+
current_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
68+
# TODO: Best way to test throttling?
69+
end
70+
end
71+
end
72+
end
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
require 'spec_helper'
5+
6+
describe Mongo::Auth::Oidc::TokenCache do
7+
let(:cache) do
8+
described_class.new
9+
end
10+
11+
describe '#invalidate' do
12+
let(:token_one) do
13+
'token_one'
14+
end
15+
16+
let(:token_two) do
17+
'token_two'
18+
end
19+
20+
context 'when the token matches the existing token' do
21+
before do
22+
cache.access_token = token_one
23+
cache.invalidate(token: token_one)
24+
end
25+
26+
it 'invalidates the token' do
27+
expect(cache.access_token).to be_nil
28+
end
29+
end
30+
31+
context 'when the token does not equal the existing token' do
32+
before do
33+
cache.access_token = token_one
34+
cache.invalidate(token: token_two)
35+
end
36+
37+
it 'does not invalidate the token' do
38+
expect(cache.access_token).to eq(token_one)
39+
end
40+
end
41+
end
42+
end

‎spec/mongo/auth/user_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
it 'raises ArgumentError' do
5151
expect do
5252
user
53-
end.to raise_error(Mongo::Auth::InvalidMechanism, ":invalid is invalid, please use one of the following mechanisms: :aws, :gssapi, :mongodb_cr, :mongodb_x509, :plain, :scram, :scram256")
53+
end.to raise_error(Mongo::Auth::InvalidMechanism, ":invalid is invalid, please use one of the following mechanisms: :aws, :gssapi, :mongodb_cr, :mongodb_oidc, :mongodb_x509, :plain, :scram, :scram256")
5454
end
5555
end
5656

‎spec/mongo/auth_spec.rb

+15
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@
77

88
describe '#get' do
99

10+
context 'when a mongodb_oidc user is provided' do
11+
12+
let(:user) do
13+
Mongo::Auth::User.new(auth_mech: :mongodb_oidc, auth_mech_properties: { environment: 'test' })
14+
end
15+
16+
let(:oidc) do
17+
described_class.get(user, double('connection'))
18+
end
19+
20+
it 'returns Oidc' do
21+
expect(oidc).to be_a(Mongo::Auth::Oidc)
22+
end
23+
end
24+
1025
context 'when a mongodb_cr user is provided' do
1126

1227
let(:user) do

‎spec/spec_tests/data/auth/connection-string.yml

+482-365
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.