Skip to content

Commit afd7e1c

Browse files
authored
PYTHON-3460 Implement OIDC SASL mechanism (#1138)
1 parent d504322 commit afd7e1c

15 files changed

+1970
-18
lines changed

.evergreen/config.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,68 @@ functions:
749749
fi
750750
PYTHON_BINARY=${PYTHON_BINARY} ASSERT_NO_URI_CREDS=true .evergreen/run-mongodb-aws-test.sh
751751
752+
"bootstrap oidc":
753+
- command: ec2.assume_role
754+
params:
755+
role_arn: ${aws_test_secrets_role}
756+
- command: shell.exec
757+
type: test
758+
params:
759+
working_dir: "src"
760+
shell: bash
761+
script: |
762+
${PREPARE_SHELL}
763+
if [ "${skip_EC2_auth_test}" = "true" ]; then
764+
echo "This platform does not support the oidc auth test, skipping..."
765+
exit 0
766+
fi
767+
768+
cd ${DRIVERS_TOOLS}/.evergreen/auth_oidc
769+
export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
770+
export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
771+
export AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}
772+
export OIDC_TOKEN_DIR=/tmp/tokens
773+
774+
. ./activate-authoidcvenv.sh
775+
python oidc_write_orchestration.py
776+
python oidc_get_tokens.py
777+
778+
"run oidc auth test with aws credentials":
779+
- command: shell.exec
780+
type: test
781+
params:
782+
working_dir: "src"
783+
shell: bash
784+
script: |
785+
${PREPARE_SHELL}
786+
if [ "${skip_EC2_auth_test}" = "true" ]; then
787+
echo "This platform does not support the oidc auth test, skipping..."
788+
exit 0
789+
fi
790+
cd ${DRIVERS_TOOLS}/.evergreen/auth_oidc
791+
mongosh setup_oidc.js
792+
- command: shell.exec
793+
type: test
794+
params:
795+
working_dir: "src"
796+
silent: true
797+
script: |
798+
# DO NOT ECHO WITH XTRACE (which PREPARE_SHELL does)
799+
cat <<'EOF' > "${PROJECT_DIRECTORY}/prepare_mongodb_oidc.sh"
800+
export OIDC_TOKEN_DIR=/tmp/tokens
801+
EOF
802+
- command: shell.exec
803+
type: test
804+
params:
805+
working_dir: "src"
806+
script: |
807+
${PREPARE_SHELL}
808+
if [ "${skip_web_identity_auth_test}" = "true" ]; then
809+
echo "This platform does not support the oidc auth test, skipping..."
810+
exit 0
811+
fi
812+
PYTHON_BINARY=${PYTHON_BINARY} ASSERT_NO_URI_CREDS=true .evergreen/run-mongodb-oidc-test.sh
813+
752814
"run aws auth test with aws credentials as environment variables":
753815
- command: shell.exec
754816
type: test
@@ -2034,6 +2096,19 @@ tasks:
20342096
- func: "run aws auth test with aws web identity credentials"
20352097
- func: "run aws ECS auth test"
20362098

2099+
- name: "oidc-auth-test-latest"
2100+
commands:
2101+
- func: "bootstrap oidc"
2102+
- func: "bootstrap mongo-orchestration"
2103+
vars:
2104+
AUTH: "auth"
2105+
ORCHESTRATION_FILE: "auth-oidc.json"
2106+
TOPOLOGY: "replica_set"
2107+
VERSION: "latest"
2108+
- func: "run oidc auth test with aws credentials"
2109+
vars:
2110+
AWS_WEB_IDENTITY_TOKEN_FILE: /tmp/tokens/test1
2111+
20372112
- name: load-balancer-test
20382113
commands:
20392114
- func: "bootstrap mongo-orchestration"
@@ -3103,6 +3178,14 @@ buildvariants:
31033178
# macOS MongoDB servers do not staple OCSP responses and only support RSA.
31043179
- name: ".ocsp-rsa !.ocsp-staple"
31053180

3181+
- matrix_name: "oidc-auth-test"
3182+
matrix_spec:
3183+
platform: [ ubuntu-20.04 ]
3184+
python-version: ["3.9"]
3185+
display_name: "MONGODB-OIDC Auth ${platform} ${python-version}"
3186+
tasks:
3187+
- name: "oidc-auth-test-latest"
3188+
31063189
- matrix_name: "aws-auth-test"
31073190
matrix_spec:
31083191
platform: [ubuntu-20.04]

.evergreen/resync-specs.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ for spec in "$@"
7070
do
7171
# Match the spec dir name, the python test dir name, and/or common abbreviations.
7272
case "$spec" in
73+
auth)
74+
cpjson auth/tests/ auth
75+
;;
7376
atlas-data-lake-testing|data_lake)
7477
cpjson atlas-data-lake-testing/tests/ data_lake
7578
;;

.evergreen/run-mongodb-oidc-test.sh

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/bin/bash
2+
3+
set -o xtrace
4+
set -o errexit # Exit the script with error if any of the commands fail
5+
6+
############################################
7+
# Main Program #
8+
############################################
9+
10+
# Supported/used environment variables:
11+
# MONGODB_URI Set the URI, including an optional username/password to use
12+
# to connect to the server via MONGODB-OIDC authentication
13+
# mechanism.
14+
# PYTHON_BINARY The Python version to use.
15+
16+
echo "Running MONGODB-OIDC authentication tests"
17+
# ensure no secrets are printed in log files
18+
set +x
19+
20+
# load the script
21+
shopt -s expand_aliases # needed for `urlencode` alias
22+
[ -s "${PROJECT_DIRECTORY}/prepare_mongodb_oidc.sh" ] && source "${PROJECT_DIRECTORY}/prepare_mongodb_oidc.sh"
23+
24+
MONGODB_URI=${MONGODB_URI:-"mongodb://localhost"}
25+
MONGODB_URI_SINGLE="${MONGODB_URI}/?authMechanism=MONGODB-OIDC"
26+
MONGODB_URI_MULTIPLE="${MONGODB_URI}:27018/?authMechanism=MONGODB-OIDC&directConnection=true"
27+
28+
if [ -z "${OIDC_TOKEN_DIR}" ]; then
29+
echo "Must specify OIDC_TOKEN_DIR"
30+
exit 1
31+
fi
32+
33+
export MONGODB_URI_SINGLE="$MONGODB_URI_SINGLE"
34+
export MONGODB_URI_MULTIPLE="$MONGODB_URI_MULTIPLE"
35+
export MONGODB_URI="$MONGODB_URI"
36+
37+
echo $MONGODB_URI_SINGLE
38+
echo $MONGODB_URI_MULTIPLE
39+
echo $MONGODB_URI
40+
41+
if [ "$ASSERT_NO_URI_CREDS" = "true" ]; then
42+
if echo "$MONGODB_URI" | grep -q "@"; then
43+
echo "MONGODB_URI unexpectedly contains user credentials!";
44+
exit 1
45+
fi
46+
fi
47+
48+
# show test output
49+
set -x
50+
51+
# Workaround macOS python 3.9 incompatibility with system virtualenv.
52+
if [ "$(uname -s)" = "Darwin" ]; then
53+
VIRTUALENV="/Library/Frameworks/Python.framework/Versions/3.9/bin/python3 -m virtualenv"
54+
else
55+
VIRTUALENV=$(command -v virtualenv)
56+
fi
57+
58+
authtest () {
59+
if [ "Windows_NT" = "$OS" ]; then
60+
PYTHON=$(cygpath -m $PYTHON)
61+
fi
62+
63+
echo "Running MONGODB-OIDC authentication tests with $PYTHON"
64+
$PYTHON --version
65+
66+
$VIRTUALENV -p $PYTHON --never-download venvoidc
67+
if [ "Windows_NT" = "$OS" ]; then
68+
. venvoidc/Scripts/activate
69+
else
70+
. venvoidc/bin/activate
71+
fi
72+
python -m pip install -U pip setuptools
73+
python -m pip install '.[aws]'
74+
python test/auth_aws/test_auth_oidc.py -v
75+
deactivate
76+
rm -rf venvoidc
77+
}
78+
79+
PYTHON=${PYTHON_BINARY:-}
80+
if [ -z "$PYTHON" ]; then
81+
echo "Cannot test without specifying PYTHON_BINARY"
82+
exit 1
83+
fi
84+
85+
authtest

pymongo/auth.py

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from bson.binary import Binary
2828
from bson.son import SON
2929
from pymongo.auth_aws import _authenticate_aws
30+
from pymongo.auth_oidc import _authenticate_oidc, _get_authenticator, _OIDCProperties
3031
from pymongo.errors import ConfigurationError, OperationFailure
3132
from pymongo.saslprep import saslprep
3233

@@ -48,6 +49,7 @@
4849
[
4950
"GSSAPI",
5051
"MONGODB-CR",
52+
"MONGODB-OIDC",
5153
"MONGODB-X509",
5254
"MONGODB-AWS",
5355
"PLAIN",
@@ -101,7 +103,7 @@ def __hash__(self):
101103

102104
def _build_credentials_tuple(mech, source, user, passwd, extra, database):
103105
"""Build and return a mechanism specific credentials tuple."""
104-
if mech not in ("MONGODB-X509", "MONGODB-AWS") and user is None:
106+
if mech not in ("MONGODB-X509", "MONGODB-AWS", "MONGODB-OIDC") and user is None:
105107
raise ConfigurationError("%s requires a username." % (mech,))
106108
if mech == "GSSAPI":
107109
if source is not None and source != "$external":
@@ -137,6 +139,32 @@ def _build_credentials_tuple(mech, source, user, passwd, extra, database):
137139
aws_props = _AWSProperties(aws_session_token=aws_session_token)
138140
# user can be None for temporary link-local EC2 credentials.
139141
return MongoCredential(mech, "$external", user, passwd, aws_props, None)
142+
elif mech == "MONGODB-OIDC":
143+
properties = extra.get("authmechanismproperties", {})
144+
request_token_callback = properties.get("request_token_callback")
145+
refresh_token_callback = properties.get("refresh_token_callback", None)
146+
provider_name = properties.get("PROVIDER_NAME", "")
147+
default_allowed = [
148+
"*.mongodb.net",
149+
"*.mongodb-dev.net",
150+
"*.mongodbgov.net",
151+
"localhost",
152+
"127.0.0.1",
153+
"::1",
154+
]
155+
allowed_hosts = properties.get("allowed_hosts", default_allowed)
156+
if not request_token_callback and provider_name != "aws":
157+
raise ConfigurationError(
158+
"authentication with MONGODB-OIDC requires providing an request_token_callback or a provider_name of 'aws'"
159+
)
160+
oidc_props = _OIDCProperties(
161+
request_token_callback=request_token_callback,
162+
refresh_token_callback=refresh_token_callback,
163+
provider_name=provider_name,
164+
allowed_hosts=allowed_hosts,
165+
)
166+
return MongoCredential(mech, "$external", user, passwd, oidc_props, None)
167+
140168
elif mech == "PLAIN":
141169
source_database = source or database or "$external"
142170
return MongoCredential(mech, source_database, user, passwd, None, None)
@@ -439,7 +467,7 @@ def _authenticate_x509(credentials, sock_info):
439467
# MONGODB-X509 is done after the speculative auth step.
440468
return
441469

442-
cmd = _X509Context(credentials).speculate_command()
470+
cmd = _X509Context(credentials, sock_info.address).speculate_command()
443471
sock_info.command("$external", cmd)
444472

445473

@@ -482,6 +510,7 @@ def _authenticate_default(credentials, sock_info):
482510
"MONGODB-CR": _authenticate_mongo_cr,
483511
"MONGODB-X509": _authenticate_x509,
484512
"MONGODB-AWS": _authenticate_aws,
513+
"MONGODB-OIDC": _authenticate_oidc,
485514
"PLAIN": _authenticate_plain,
486515
"SCRAM-SHA-1": functools.partial(_authenticate_scram, mechanism="SCRAM-SHA-1"),
487516
"SCRAM-SHA-256": functools.partial(_authenticate_scram, mechanism="SCRAM-SHA-256"),
@@ -490,15 +519,16 @@ def _authenticate_default(credentials, sock_info):
490519

491520

492521
class _AuthContext(object):
493-
def __init__(self, credentials):
522+
def __init__(self, credentials, address):
494523
self.credentials = credentials
495524
self.speculative_authenticate = None
525+
self.address = address
496526

497527
@staticmethod
498-
def from_credentials(creds):
528+
def from_credentials(creds, address):
499529
spec_cls = _SPECULATIVE_AUTH_MAP.get(creds.mechanism)
500530
if spec_cls:
501-
return spec_cls(creds)
531+
return spec_cls(creds, address)
502532
return None
503533

504534
def speculate_command(self):
@@ -512,8 +542,8 @@ def speculate_succeeded(self):
512542

513543

514544
class _ScramContext(_AuthContext):
515-
def __init__(self, credentials, mechanism):
516-
super(_ScramContext, self).__init__(credentials)
545+
def __init__(self, credentials, address, mechanism):
546+
super(_ScramContext, self).__init__(credentials, address)
517547
self.scram_data = None
518548
self.mechanism = mechanism
519549

@@ -534,16 +564,30 @@ def speculate_command(self):
534564
return cmd
535565

536566

567+
class _OIDCContext(_AuthContext):
568+
def speculate_command(self):
569+
authenticator = _get_authenticator(self.credentials, self.address)
570+
cmd = authenticator.auth_start_cmd(False)
571+
if cmd is None:
572+
return
573+
cmd["db"] = self.credentials.source
574+
return cmd
575+
576+
537577
_SPECULATIVE_AUTH_MAP: Mapping[str, Callable] = {
538578
"MONGODB-X509": _X509Context,
539579
"SCRAM-SHA-1": functools.partial(_ScramContext, mechanism="SCRAM-SHA-1"),
540580
"SCRAM-SHA-256": functools.partial(_ScramContext, mechanism="SCRAM-SHA-256"),
581+
"MONGODB-OIDC": _OIDCContext,
541582
"DEFAULT": functools.partial(_ScramContext, mechanism="SCRAM-SHA-256"),
542583
}
543584

544585

545-
def authenticate(credentials, sock_info):
586+
def authenticate(credentials, sock_info, reauthenticate=False):
546587
"""Authenticate sock_info."""
547588
mechanism = credentials.mechanism
548589
auth_func = _AUTH_MAP[mechanism]
549-
auth_func(credentials, sock_info)
590+
if mechanism == "MONGODB-OIDC":
591+
_authenticate_oidc(credentials, sock_info, reauthenticate)
592+
else:
593+
auth_func(credentials, sock_info)

0 commit comments

Comments
 (0)