Skip to content

Commit 5ad5942

Browse files
committed
RUBY-3303 Add OIDC machine workflow auth
1 parent 3c5dc93 commit 5ad5942

34 files changed

+1760
-378
lines changed

.evergreen/config.yml

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,34 @@ 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 prose 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+
ENVIRONMENT: ${ENVIRONMENT}
467+
RVM_RUBY: ${RVM_RUBY}
468+
args:
469+
- .evergreen/run-tests-oidc-prose.sh
470+
471+
"run oidc unified 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-unified.sh
484+
457485
pre:
458486
- func: "fetch source"
459487
- func: "create expansions"
@@ -751,6 +779,77 @@ task_groups:
751779
tasks:
752780
- testazurekms-task
753781

782+
- name: test_oidc_task_group
783+
setup_group:
784+
- func: fetch source
785+
- func: create expansions
786+
- command: ec2.assume_role
787+
params:
788+
role_arn: ${aws_test_secrets_role}
789+
- command: subprocess.exec
790+
params:
791+
binary: bash
792+
include_expansions_in_env:
793+
- AWS_ACCESS_KEY_ID
794+
- AWS_SECRET_ACCESS_KEY
795+
- AWS_SESSION_TOKEN
796+
env:
797+
MONGODB_VERSION: '8.0'
798+
args:
799+
- ${DRIVERS_TOOLS}/.evergreen/auth_oidc/setup.sh
800+
setup_group_can_fail_task: true
801+
setup_group_timeout_secs: 1800
802+
tasks:
803+
- oidc-auth-test-latest
804+
805+
- name: test_oidc_azure_task_group
806+
setup_group:
807+
- func: fetch source
808+
- func: create expansions
809+
- command: shell.exec
810+
params:
811+
shell: bash
812+
script: |-
813+
set -o errexit
814+
${PREPARE_SHELL}
815+
export AZUREOIDC_VMNAME_PREFIX="RUBY_DRIVER"
816+
$DRIVERS_TOOLS/.evergreen/auth_oidc/azure/setup.sh
817+
teardown_task:
818+
- command: shell.exec
819+
params:
820+
shell: bash
821+
script: |-
822+
${PREPARE_SHELL}
823+
$DRIVERS_TOOLS/.evergreen/auth_oidc/azure/teardown.sh
824+
setup_group_can_fail_task: true
825+
setup_group_timeout_secs: 1800
826+
tasks:
827+
- oidc-auth-test-azure-latest
828+
829+
- name: test_oidc_gcp_task_group
830+
setup_group:
831+
- func: fetch source
832+
- func: create expansions
833+
- command: shell.exec
834+
params:
835+
shell: bash
836+
script: |-
837+
set -o errexit
838+
${PREPARE_SHELL}
839+
export GCPOIDC_VMNAME_PREFIX="RUBY_DRIVER"
840+
$DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/setup.sh
841+
teardown_task:
842+
- command: shell.exec
843+
params:
844+
shell: bash
845+
script: |-
846+
${PREPARE_SHELL}
847+
$DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/teardown.sh
848+
setup_group_can_fail_task: true
849+
setup_group_timeout_secs: 1800
850+
tasks:
851+
- oidc-auth-test-gcp-latest
852+
754853
tasks:
755854
- name: "test-atlas"
756855
commands:
@@ -895,8 +994,35 @@ tasks:
895994
LAMBDA_STACK_NAME: "dbx-ruby-lambda"
896995
RVM_RUBY: ruby-3.2
897996
MONGODB_URI: ${MONGODB_URI}
898-
axes:
899997

998+
- name: oidc-auth-test-latest
999+
commands:
1000+
- func: "run oidc prose tests"
1001+
vars:
1002+
ENVIRONMENT: test
1003+
- func: "run oidc unified tests"
1004+
vars:
1005+
ENVIRONMENT: test
1006+
1007+
- name: oidc-auth-test-azure-latest
1008+
commands:
1009+
- func: "run oidc prose tests"
1010+
vars:
1011+
ENVIRONMENT: azure
1012+
- func: "run oidc unified tests"
1013+
vars:
1014+
ENVIRONMENT: azure
1015+
1016+
- name: oidc-auth-test-gcp-latest
1017+
commands:
1018+
- func: "run oidc prose tests"
1019+
vars:
1020+
ENVIRONMENT: gcp
1021+
- func: "run oidc unified tests"
1022+
vars:
1023+
ENVIRONMENT: gcp
1024+
1025+
axes:
9001026
- id: preload
9011027
display_name: Preload server
9021028
values:
@@ -1898,3 +2024,16 @@ buildvariants:
18982024
display_name: "AWS Lambda"
18992025
tasks:
19002026
- name: test_aws_lambda_task_group
2027+
2028+
- matrix_name: test-oidc-variant
2029+
matrix_spec:
2030+
ruby: "ruby-3.2"
2031+
fle: helper
2032+
topology: standalone
2033+
os: ubuntu2004
2034+
mongodb-version: latest
2035+
display_name: "OIDC auth tests: latest ruby-3.2"
2036+
tasks:
2037+
- test_oidc_task_group
2038+
- test_oidc_azure_task_group
2039+
- test_oidc_gcp_task_group

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

Lines changed: 22 additions & 0 deletions
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

Lines changed: 3 additions & 1 deletion
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

Lines changed: 86 additions & 0 deletions
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

Lines changed: 76 additions & 0 deletions
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

0 commit comments

Comments
 (0)