-
Notifications
You must be signed in to change notification settings - Fork 1.1k
PYTHON-3460 Implement OIDC SASL mechanism #1138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 98 commits
2381380
5048aea
17972eb
0380ded
950df79
ab10e68
e9a8992
883d427
ccf5a3f
be21e22
1595f31
8bd23c6
0e93762
eef2712
8e5638e
905b89d
43d49ad
e93c4c0
324437e
0ddeaf3
b9c0d47
50b72e1
0313d6c
b02a969
8d6d503
1a12de9
f408386
b7f161a
2826753
57046f8
9f4fb49
62d78dc
8bff0ff
35182f3
7ac2fed
8ce95bc
0ecfba4
b42cc7d
834c130
818911d
3ccd850
a59ba53
5f59c31
2cc0a62
7479a24
caabfb6
b236f2a
eab4d83
933be5f
20aecb7
7ed72fe
c4c64bc
3acf277
0da3037
370ef94
10e9f20
0abc433
93a4884
b0d1e3b
256915a
51c6639
b78e404
ce250de
3339837
2c33731
529cf63
5f0fde0
765dde6
6186ed9
c2e91e2
ca6dd7d
5f730ec
f1998d6
ba2253b
b023041
3b6288b
a574237
3607f72
75a707b
35fc668
3ceda8a
6976ef8
43a5269
40ed893
a46ba28
c7df390
02ce52d
28a804b
19dda74
9c33a02
c21771f
bad4527
7beac7a
8c8088a
6c45619
500c98f
11261b9
f4ebaf6
f584885
beb2b24
6d3ebe2
e46047d
3454bab
8e8912d
2133125
e9024ec
f522d0d
b0c0919
5828220
a78ad30
ef991f9
4cf5110
a354ce6
66f2276
710add4
b98aa5c
5fe805f
eb3ced0
c7d33b2
d23ef6b
b431cdc
504d2b1
a089dc0
7c56888
7bb678f
bd53fdc
b13427e
8d24dd9
c73be94
1924f3e
770e4f6
828e22a
c719618
1739968
f4d6f24
cbadc70
109a204
7baf5a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
#!/bin/bash | ||
|
||
set -o xtrace | ||
set -o errexit # Exit the script with error if any of the commands fail | ||
|
||
############################################ | ||
# Main Program # | ||
############################################ | ||
|
||
# Supported/used environment variables: | ||
# MONGODB_URI Set the URI, including an optional username/password to use | ||
# to connect to the server via MONGODB-OIDC authentication | ||
# mechanism. | ||
# PYTHON_BINARY The Python version to use. | ||
|
||
echo "Running MONGODB-OIDC authentication tests" | ||
# ensure no secrets are printed in log files | ||
set +x | ||
|
||
# load the script | ||
shopt -s expand_aliases # needed for `urlencode` alias | ||
[ -s "${PROJECT_DIRECTORY}/prepare_mongodb_oidc.sh" ] && source "${PROJECT_DIRECTORY}/prepare_mongodb_oidc.sh" | ||
|
||
MONGODB_URI=${MONGODB_URI:-"mongodb://localhost"} | ||
MONGODB_URI_SINGLE="${MONGODB_URI}/?authMechanism=MONGODB-OIDC" | ||
MONGODB_URI_MULTIPLE="${MONGODB_URI}:27018/?authMechanism=MONGODB-OIDC&directConnection=true" | ||
|
||
if [ -z "${OIDC_TOKEN_DIR}" ]; then | ||
echo "Must specify OIDC_TOKEN_DIR" | ||
exit 1 | ||
fi | ||
|
||
export MONGODB_URI_SINGLE="$MONGODB_URI_SINGLE" | ||
export MONGODB_URI_MULTIPLE="$MONGODB_URI_MULTIPLE" | ||
export MONGODB_URI="$MONGODB_URI" | ||
|
||
echo $MONGODB_URI_SINGLE | ||
echo $MONGODB_URI_MULTIPLE | ||
echo $MONGODB_URI | ||
|
||
if [ "$ASSERT_NO_URI_CREDS" = "true" ]; then | ||
if echo "$MONGODB_URI" | grep -q "@"; then | ||
echo "MONGODB_URI unexpectedly contains user credentials!"; | ||
exit 1 | ||
fi | ||
fi | ||
|
||
# show test output | ||
set -x | ||
|
||
# Workaround macOS python 3.9 incompatibility with system virtualenv. | ||
if [ "$(uname -s)" = "Darwin" ]; then | ||
# TODO: change back to 3.9 before merging. | ||
VIRTUALENV="/Library/Frameworks/Python.framework/Versions/3.10/bin/python3 -m virtualenv" | ||
else | ||
VIRTUALENV=$(command -v virtualenv) | ||
fi | ||
|
||
authtest () { | ||
if [ "Windows_NT" = "$OS" ]; then | ||
PYTHON=$(cygpath -m $PYTHON) | ||
fi | ||
|
||
echo "Running MONGODB-OIDC authentication tests with $PYTHON" | ||
$PYTHON --version | ||
|
||
$VIRTUALENV -p $PYTHON --never-download venvoidc | ||
if [ "Windows_NT" = "$OS" ]; then | ||
. venvoidc/Scripts/activate | ||
else | ||
. venvoidc/bin/activate | ||
fi | ||
python -m pip install -U pip setuptools | ||
python -m pip install '.[aws]' | ||
python test/auth_aws/test_auth_oidc.py -v | ||
deactivate | ||
rm -rf venvoidc | ||
} | ||
|
||
PYTHON=${PYTHON_BINARY:-} | ||
if [ -z "$PYTHON" ]; then | ||
echo "Cannot test without specifying PYTHON_BINARY" | ||
exit 1 | ||
fi | ||
|
||
authtest |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -384,3 +384,127 @@ would be:: | |
.. _Assume Role: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html | ||
.. _EC2 instance: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html | ||
.. _environment variables: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime | ||
|
||
|
||
.. _oidc_sasl: | ||
|
||
MONGODB-OIDC | ||
------------ | ||
.. versionadded:: 4.4 | ||
|
||
The MONGODB-OIDC authentication mechanism is available in MongoDB Enterprise 7.0+. | ||
|
||
AWS OIDC Support | ||
~~~~~~~~~~~~~~~~ | ||
|
||
PyMongo supports automatic authentication when AWS OIDC credentials are | ||
available, by installing pymongo with the | ||
``aws`` extra:: | ||
|
||
$ python -m pip install 'pymongo[aws]' | ||
|
||
A sample URI would be: | ||
|
||
>>> from pymongo import MongoClient | ||
>>> uri = "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws") | ||
>>> client = MongoClient(uri) | ||
|
||
The driver will use the authentication token from the file given by the | ||
``AWS_WEB_IDENTITY_TOKEN_FILE`` environment variable provided by AWS to | ||
authenticate with the server. | ||
|
||
Callback-based OIDC Support | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
PyMongo supports user-provided callbacks for OIDC, which are are given to the | ||
``MongoClient``. The ``on_oidc_request_callback`` is intended to accept | ||
information about the Identity Provider, and return credentials that are used | ||
to authenticate with the server, usually through a browser interaction with | ||
the user. The callback must be of the form:: | ||
|
||
def request_callback(ProviderInfo, timeout_seconds) -> TokenResult: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: add type annotations for provider_info and timeout_seconds. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We actually won't have public-facing docs until post-7.0, I removed the changes to this file. |
||
... | ||
return dict(access_token=...) | ||
|
||
Where ``ProviderInfo`` is a dictionary of the following form:: | ||
|
||
authorization_endpoint: | ||
description: >- | ||
URL where the IDP may be contacted for end user | ||
authentication and authorization code generation. | ||
type: string | ||
optional: true # Req if device_authorization_endpoint not present | ||
token_endpoint: | ||
description: >- | ||
URL where the IDP may be contacted for authorization | ||
code <=> ID/access token exchange. | ||
type: string | ||
optional: true # Req if device_authorization_endpoint not present | ||
device_authorization_endpoint: | ||
description: >- | ||
URL where the IDP may be contacted for device | ||
authentication and authorization code generation. | ||
type: string | ||
optional: true # Req if authorization_endpoint not present | ||
client_id: | ||
description: "Unique client ID for this OIDC client" | ||
type: string | ||
client_ecret: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. client_ecret -> client_secret There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. N/A |
||
description: "Secret used when communicating with IDP" | ||
type: string | ||
optional: true | ||
request_scopes: | ||
description: "Additional scopes to request from IDP" | ||
type: array<string> | ||
optional: true | ||
|
||
And ``TokenResult`` is a dictionary of the following form:: | ||
|
||
access_token: | ||
description: "The OIDC access token" | ||
type: string | ||
expires_in_seconds: | ||
description: "The expiration time in seconds from the current time" | ||
type: int | ||
optional: true | ||
refresh_token: | ||
description: "The OIDC refresh token" | ||
type: str | ||
optional: true | ||
|
||
And ``timeout_seconds`` will always be 300 (5 minutes). An example | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. " There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. N/A |
||
client would be:: | ||
|
||
>>> from pymongo import MongoClient | ||
>>> uri = "mongodb://localhost/?authMechanism=MONGODB-OIDC") | ||
>>> client = MongoClient(uri, on_oidc_request_callback=my_callback) | ||
|
||
If the identity provider supports refresh, a refresh callback can also | ||
be provided. If a refresh callback is provided, it will be called | ||
if ``expires_in_seconds`` was given in the request response and is | ||
within 5 minutes, or the server raises a ``ReAuthenticationRequired`` | ||
error during an operation. The refresh callback must of the form:: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2 callbacks leads me to think an ABC is a better approach here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍🏼 |
||
|
||
|
||
def request_callback(ProviderInfo, TokenResult, timeout_seconds) -> TokenResult: | ||
... | ||
return dict(access_token=...) | ||
|
||
Where ``ProviderInfo``, ``timeout_seconds`` and the return value are of | ||
the same form as the request callback, and the ``TokenResult`` parameter | ||
is the result of the request callback, which will contain the ``refresh_token`` | ||
if it was provided. An example using both callbacks would be:: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand why there are two callbacks. Can't the user just always implement the refresh_token_callback and then the driver can pass refresh_token=None on the initial request? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The presence or lack thereof of the refresh callback affects the mechanism behavior. |
||
|
||
>>> from pymongo import MongoClient | ||
>>> uri = "mongodb://localhost/?authMechanism=MONGODB-OIDC") | ||
>>> client = MongoClient(uri, on_oidc_request_callback=my_request_callback, | ||
... on_oidc_refresh_callback=my_refresh_callback) | ||
|
||
Note: when multiple identity providers | ||
are configured on the server, a ``username`` must be provided, which is the | ||
Principal Name used on the provider. For example:: | ||
|
||
>>> from pymongo import MongoClient | ||
>>> uri = "mongodb://my_username@localhost/?authMechanism=MONGODB-OIDC") | ||
>>> client = MongoClient(uri, on_oidc_request_callback=my_request_callback, | ||
... on_oidc_refresh_callback=my_refresh_callback) |
Uh oh!
There was an error while loading. Please reload this page.