30
22
except ImportError:
31
23
HAVE_KERBEROS = False
25
from base64 import standard_b64decode, standard_b64encode
26
from collections import namedtuple
27
from hashlib import md5, sha1
28
from random import SystemRandom
33
30
from bson.binary import Binary
34
from bson.py3compat import b
31
from bson.py3compat import b, string_type, _unicode, PY3
35
32
from bson.son import SON
36
33
from pymongo.errors import ConfigurationError, OperationFailure
39
MECHANISMS = frozenset(['GSSAPI', 'MONGODB-CR', 'MONGODB-X509', 'PLAIN'])
36
MECHANISMS = frozenset(
37
['GSSAPI', 'MONGODB-CR', 'MONGODB-X509', 'PLAIN', 'SCRAM-SHA-1', 'DEFAULT'])
40
38
"""The authentication mechanisms supported by PyMongo."""
41
MongoCredential = namedtuple(
43
['mechanism', 'source', 'username', 'password', 'mechanism_properties'])
44
"""A hashable namedtuple of values used for authentication."""
47
GSSAPIProperties = namedtuple('GSSAPIProperties', ['service_name'])
48
"""Mechanism properties for GSSAPI authentication."""
43
51
def _build_credentials_tuple(mech, source, user, passwd, extra):
44
52
"""Build and return a mechanism specific credentials tuple.
46
55
if mech == 'GSSAPI':
47
gsn = extra.get('gssapiservicename', 'mongodb')
56
properties = extra.get('authmechanismproperties', {})
57
service_name = properties.get('SERVICE_NAME', 'mongodb')
58
props = GSSAPIProperties(service_name=service_name)
48
59
# No password, source is always $external.
49
return (mech, '$external', user, gsn)
60
return MongoCredential(mech, '$external', user, None, props)
50
61
elif mech == 'MONGODB-X509':
51
return (mech, '$external', user)
52
return (mech, source, user, passwd)
62
return MongoCredential(mech, '$external', user, None, None)
65
raise ConfigurationError("A password is required.")
66
return MongoCredential(mech, source, user, _unicode(passwd), None)
71
"""XOR two byte strings together (python 3.x)."""
72
return b"".join([bytes([x ^ y]) for x, y in zip(fir, sec)])
75
_from_bytes = int.from_bytes
76
_to_bytes = int.to_bytes
78
from binascii import (hexlify as _hexlify,
79
unhexlify as _unhexlify)
83
"""XOR two byte strings together (python 2.x)."""
84
return b"".join([chr(ord(x) ^ ord(y)) for x, y in zip(fir, sec)])
87
def _from_bytes(value, dummy, int=int, _hexlify=_hexlify):
88
"""An implementation of int.from_bytes for python 2.x."""
89
return int(_hexlify(value), 16)
92
def _to_bytes(value, dummy0, dummy1, _unhexlify=_unhexlify):
93
"""An implementation of int.to_bytes for python 2.x."""
94
return _unhexlify('%040x' % value)
98
# The fastest option, if it's been compiled to use OpenSSL's HMAC.
99
from backports.pbkdf2 import pbkdf2_hmac
101
def _hi(data, salt, iterations):
102
return pbkdf2_hmac('sha1', data, salt, iterations)
106
# Python 2.7.8+, or Python 3.4+.
107
from hashlib import pbkdf2_hmac
109
def _hi(data, salt, iterations):
110
return pbkdf2_hmac('sha1', data, salt, iterations)
114
def _hi(data, salt, iterations):
115
"""A simple implementation of PBKDF2."""
116
mac = hmac.HMAC(data, None, sha1)
118
def _digest(msg, mac=mac):
119
"""Get a digest for msg."""
124
from_bytes = _from_bytes
127
_u1 = _digest(salt + b'\x00\x00\x00\x01')
128
_ui = from_bytes(_u1, 'big')
129
for _ in range(iterations - 1):
131
_ui ^= from_bytes(_u1, 'big')
132
return to_bytes(_ui, 20, 'big')
135
def _parse_scram_response(response):
136
"""Split a scram response into key, value pairs."""
137
return dict(item.split(b"=", 1) for item in response.split(b","))
140
def _authenticate_scram_sha1(credentials, sock_info):
141
"""Authenticate using SCRAM-SHA-1."""
142
username = credentials.username
143
password = credentials.password
144
source = credentials.source
150
user = username.encode("utf-8").replace(b"=", b"=3D").replace(b",", b"=2C")
151
nonce = standard_b64encode(
152
(("%s" % (SystemRandom().random(),))[2:]).encode("utf-8"))
153
first_bare = b"n=" + user + b",r=" + nonce
155
cmd = SON([('saslStart', 1),
156
('mechanism', 'SCRAM-SHA-1'),
157
('payload', Binary(b"n,," + first_bare)),
158
('autoAuthorize', 1)])
159
res = sock_info.command(source, cmd)
161
server_first = res['payload']
162
parsed = _parse_scram_response(server_first)
163
iterations = int(parsed[b'i'])
165
rnonce = parsed[b'r']
166
if not rnonce.startswith(nonce):
167
raise OperationFailure("Server returned an invalid nonce.")
169
without_proof = b"c=biws,r=" + rnonce
170
salted_pass = _hi(_password_digest(username, password).encode("utf-8"),
171
standard_b64decode(salt),
173
client_key = _hmac(salted_pass, b"Client Key", _sha1).digest()
174
stored_key = _sha1(client_key).digest()
175
auth_msg = b",".join((first_bare, server_first, without_proof))
176
client_sig = _hmac(stored_key, auth_msg, _sha1).digest()
177
client_proof = b"p=" + standard_b64encode(_xor(client_key, client_sig))
178
client_final = b",".join((without_proof, client_proof))
180
server_key = _hmac(salted_pass, b"Server Key", _sha1).digest()
181
server_sig = standard_b64encode(
182
_hmac(server_key, auth_msg, _sha1).digest())
184
cmd = SON([('saslContinue', 1),
185
('conversationId', res['conversationId']),
186
('payload', Binary(client_final))])
187
res = sock_info.command(source, cmd)
189
parsed = _parse_scram_response(res['payload'])
190
if parsed[b'v'] != server_sig:
191
raise OperationFailure("Server returned an invalid signature.")
193
# Depending on how it's configured, Cyrus SASL (which the server uses)
194
# requires a third empty challenge.
196
cmd = SON([('saslContinue', 1),
197
('conversationId', res['conversationId']),
198
('payload', Binary(b''))])
199
res = sock_info.command(source, cmd)
201
raise OperationFailure('SASL conversation failed to complete.')
55
204
def _password_digest(username, password):
56
205
"""Get a password digest to use for authentication.
58
if not isinstance(password, basestring):
59
raise TypeError("password must be an instance "
60
"of %s" % (basestring.__name__,))
207
if not isinstance(password, string_type):
208
raise TypeError("password must be an "
209
"instance of %s" % (string_type.__name__,))
61
210
if len(password) == 0:
62
211
raise ValueError("password can't be empty")
63
if not isinstance(username, basestring):
64
raise TypeError("username must be an instance "
65
"of %s" % (basestring.__name__,))
212
if not isinstance(username, string_type):
213
raise TypeError("password must be an "
214
"instance of %s" % (string_type.__name__,))
68
217
data = "%s:mongo:%s" % (username, password)
69
218
md5hash.update(data.encode('utf-8'))
70
return unicode(md5hash.hexdigest())
219
return _unicode(md5hash.hexdigest())
73
222
def _auth_key(nonce, username, password):
74
223
"""Get an auth key to use for authentication.
76
225
digest = _password_digest(username, password)
78
data = "%s%s%s" % (nonce, unicode(username), digest)
227
data = "%s%s%s" % (nonce, username, digest)
79
228
md5hash.update(data.encode('utf-8'))
80
return unicode(md5hash.hexdigest())
83
def _authenticate_gssapi(credentials, sock_info, cmd_func):
229
return _unicode(md5hash.hexdigest())
232
def _authenticate_gssapi(credentials, sock_info):
84
233
"""Authenticate using GSSAPI.
235
if not HAVE_KERBEROS:
236
raise ConfigurationError('The "kerberos" module must be '
237
'installed to use GSSAPI authentication.')
87
dummy, username, gsn = credentials
240
username = credentials.username
241
gsn = credentials.mechanism_properties.service_name
88
242
# Starting here and continuing through the while loop below - establish
89
243
# the security context. See RFC 4752, Section 3.1, first paragraph.
90
244
result, ctx = kerberos.authGSSClientInit(
151
305
cmd = SON([('saslContinue', 1),
152
306
('conversationId', response['conversationId']),
153
307
('payload', payload)])
154
response, _ = cmd_func(sock_info, '$external', cmd)
308
sock_info.command('$external', cmd)
157
311
kerberos.authGSSClientClean(ctx)
159
except kerberos.KrbError, exc:
313
except kerberos.KrbError as exc:
160
314
raise OperationFailure(str(exc))
163
def _authenticate_plain(credentials, sock_info, cmd_func):
317
def _authenticate_plain(credentials, sock_info):
164
318
"""Authenticate using SASL PLAIN (RFC 4616)
166
source, username, password = credentials
320
source = credentials.source
321
username = credentials.username
322
password = credentials.password
167
323
payload = ('\x00%s\x00%s' % (username, password)).encode('utf-8')
168
324
cmd = SON([('saslStart', 1),
169
325
('mechanism', 'PLAIN'),
170
326
('payload', Binary(payload)),
171
327
('autoAuthorize', 1)])
172
cmd_func(sock_info, source, cmd)
175
def _authenticate_cram_md5(credentials, sock_info, cmd_func):
328
sock_info.command(source, cmd)
331
def _authenticate_cram_md5(credentials, sock_info):
176
332
"""Authenticate using CRAM-MD5 (RFC 2195)
178
source, username, password = credentials
334
source = credentials.source
335
username = credentials.username
336
password = credentials.password
179
337
# The password used as the mac key is the
180
338
# same as what we use for MONGODB-CR
181
339
passwd = _password_digest(username, password)
182
340
cmd = SON([('saslStart', 1),
183
341
('mechanism', 'CRAM-MD5'),
184
('payload', Binary(b(''))),
342
('payload', Binary(b'')),
185
343
('autoAuthorize', 1)])
186
response, _ = cmd_func(sock_info, source, cmd)
344
response = sock_info.command(source, cmd)
187
345
# MD5 as implicit default digest for digestmod is deprecated
189
mac = hmac.HMAC(key=passwd.encode('utf-8'), digestmod=_DMOD)
347
mac = hmac.HMAC(key=passwd.encode('utf-8'), digestmod=md5)
190
348
mac.update(response['payload'])
191
challenge = username.encode('utf-8') + b(' ') + b(mac.hexdigest())
349
challenge = username.encode('utf-8') + b' ' + b(mac.hexdigest())
192
350
cmd = SON([('saslContinue', 1),
193
351
('conversationId', response['conversationId']),
194
352
('payload', Binary(challenge))])
195
cmd_func(sock_info, source, cmd)
198
def _authenticate_x509(credentials, sock_info, cmd_func):
353
sock_info.command(source, cmd)
356
def _authenticate_x509(credentials, sock_info):
199
357
"""Authenticate using MONGODB-X509.
201
dummy, username = credentials
202
359
query = SON([('authenticate', 1),
203
360
('mechanism', 'MONGODB-X509'),
205
cmd_func(sock_info, '$external', query)
208
def _authenticate_mongo_cr(credentials, sock_info, cmd_func):
361
('user', credentials.username)])
362
sock_info.command('$external', query)
365
def _authenticate_mongo_cr(credentials, sock_info):
209
366
"""Authenticate using MONGODB-CR.
211
source, username, password = credentials
368
source = credentials.source
369
username = credentials.username
370
password = credentials.password
213
response, _ = cmd_func(sock_info, source, {'getnonce': 1})
372
response = sock_info.command(source, {'getnonce': 1})
214
373
nonce = response['nonce']
215
374
key = _auth_key(nonce, username, password)