Skip to content

Commit efe8cc3

Browse files
authored
PYTHON-4256 OIDC Spec Cleanup (#1556)
1 parent 8be31bf commit efe8cc3

8 files changed

+688
-953
lines changed

pymongo/auth_oidc.py

+32-12
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from pymongo._csot import remaining
2929
from pymongo._gcp_helpers import _get_gcp_response
3030
from pymongo.errors import ConfigurationError, OperationFailure
31+
from pymongo.helpers import _AUTHENTICATION_FAILURE_CODE
3132

3233
if TYPE_CHECKING:
3334
from pymongo.auth import MongoCredential
@@ -37,7 +38,7 @@
3738
@dataclass
3839
class OIDCIdPInfo:
3940
issuer: str
40-
clientId: str
41+
clientId: Optional[str] = field(default=None)
4142
requestScopes: Optional[list[str]] = field(default=None)
4243

4344

@@ -189,30 +190,43 @@ def get_spec_auth_cmd(self) -> Optional[MutableMapping[str, Any]]:
189190

190191
def _authenticate_machine(self, conn: Connection) -> Mapping[str, Any]:
191192
# If there is a cached access token, try to authenticate with it. If
192-
# authentication fails, it's possible the cached access token is expired. In
193-
# that case, invalidate the access token, fetch a new access token, and try
194-
# to authenticate again.
193+
# authentication fails with error code 18, invalidate the access token,
194+
# fetch a new access token, and try to authenticate again. If authentication
195+
# fails for any other reason, raise the error to the user.
195196
if self.access_token:
196197
try:
197198
return self._sasl_start_jwt(conn)
198-
except Exception: # noqa: S110
199-
pass
199+
except OperationFailure as e:
200+
if self._is_auth_error(e):
201+
return self._authenticate_machine(conn)
202+
raise
200203
return self._sasl_start_jwt(conn)
201204

202205
def _authenticate_human(self, conn: Connection) -> Optional[Mapping[str, Any]]:
203206
# If we have a cached access token, try a JwtStepRequest.
207+
# authentication fails with error code 18, invalidate the access token,
208+
# and try to authenticate again. If authentication fails for any other
209+
# reason, raise the error to the user.
204210
if self.access_token:
205211
try:
206212
return self._sasl_start_jwt(conn)
207-
except Exception: # noqa: S110
208-
pass
213+
except OperationFailure as e:
214+
if self._is_auth_error(e):
215+
return self._authenticate_human(conn)
216+
raise
209217

210218
# If we have a cached refresh token, try a JwtStepRequest with that.
219+
# If authentication fails with error code 18, invalidate the access and
220+
# refresh tokens, and try to authenticate again. If authentication fails for
221+
# any other reason, raise the error to the user.
211222
if self.refresh_token:
212223
try:
213224
return self._sasl_start_jwt(conn)
214-
except Exception: # noqa: S110
215-
pass
225+
except OperationFailure as e:
226+
if self._is_auth_error(e):
227+
self.refresh_token = None
228+
return self._authenticate_human(conn)
229+
raise
216230

217231
# Start a new Two-Step SASL conversation.
218232
# Run a PrincipalStepRequest to get the IdpInfo.
@@ -280,10 +294,16 @@ def _get_access_token(self) -> Optional[str]:
280294
def _run_command(self, conn: Connection, cmd: MutableMapping[str, Any]) -> Mapping[str, Any]:
281295
try:
282296
return conn.command("$external", cmd, no_reauth=True) # type: ignore[call-arg]
283-
except OperationFailure:
284-
self._invalidate(conn)
297+
except OperationFailure as e:
298+
if self._is_auth_error(e):
299+
self._invalidate(conn)
285300
raise
286301

302+
def _is_auth_error(self, err: Exception) -> bool:
303+
if not isinstance(err, OperationFailure):
304+
return False
305+
return err.code == _AUTHENTICATION_FAILURE_CODE
306+
287307
def _invalidate(self, conn: Connection) -> None:
288308
# Ignore the invalidation if a token gen id is given and is less than our
289309
# current token gen id.

pymongo/common.py

-1
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,6 @@ def validate_read_preference_tags(name: str, value: Any) -> list[dict[str, str]]
426426
"AWS_SESSION_TOKEN",
427427
"ENVIRONMENT",
428428
"TOKEN_RESOURCE",
429-
"ALLOWED_HOSTS",
430429
]
431430
)
432431

pymongo/helpers.py

+3
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@
9090
# Server code raised when re-authentication is required
9191
_REAUTHENTICATION_REQUIRED_CODE: int = 391
9292

93+
# Server code raised when authentication fails.
94+
_AUTHENTICATION_FAILURE_CODE: int = 18
95+
9396

9497
def _gen_index_name(keys: _IndexList) -> str:
9598
"""Generate an index name from the set of fields it is over."""

test/auth/legacy/connection-string.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,12 @@
497497
"valid": false,
498498
"credential": null
499499
},
500+
{
501+
"description": "should throw an exception custom callback is chosen but no callback is provided (MONGODB-OIDC)",
502+
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:custom",
503+
"valid": false,
504+
"credential": null
505+
},
500506
{
501507
"description": "should throw an exception if neither provider nor callbacks specified (MONGODB-OIDC)",
502508
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC",
@@ -573,4 +579,4 @@
573579
"credential": null
574580
}
575581
]
576-
}
582+
}

0 commit comments

Comments
 (0)