Merge pull request #68 from torarnv/add-support-for-2fa
Add support for two-factor authentication (2FA)
This commit is contained in:
commit
ab889e7232
3 changed files with 102 additions and 6 deletions
30
README.rst
30
README.rst
|
@ -36,6 +36,36 @@ If you would like to delete a password stored in your system keyring, you can cl
|
||||||
|
|
||||||
>>> icloud --username=jappleseed@apple.com --delete-from-keyring
|
>>> icloud --username=jappleseed@apple.com --delete-from-keyring
|
||||||
|
|
||||||
|
*******************************
|
||||||
|
Two-factor authentication (2FA)
|
||||||
|
*******************************
|
||||||
|
|
||||||
|
If you have enabled two-factor authentication for the account you will have to do some extra work:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
if icloud.requires_2fa:
|
||||||
|
print "Two-factor authentication required. Your trusted devices are:"
|
||||||
|
|
||||||
|
devices = icloud.trusted_devices
|
||||||
|
for i, device in enumerate(devices):
|
||||||
|
print " %s: %s" % (i, device.get('deviceName',
|
||||||
|
"SMS to %s" % device.get('phoneNumber')))
|
||||||
|
|
||||||
|
device = click.prompt('Which device would you like to use?', default=0)
|
||||||
|
device = devices[device]
|
||||||
|
if not icloud.send_verification_code(device):
|
||||||
|
print "Failed to send verification code"
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
code = click.prompt('Please enter validation code')
|
||||||
|
if not icloud.validate_verification_code(device, code):
|
||||||
|
print "Failed to verify verification code"
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
Note: Both regular login and two-factor authentication will expire after an interval set by Apple, at which point you will have to re-authenticate. This interval is currently two months.
|
||||||
|
>>>>>>> Add support for two-factor authentication
|
||||||
|
|
||||||
=======
|
=======
|
||||||
Devices
|
Devices
|
||||||
=======
|
=======
|
||||||
|
|
|
@ -12,7 +12,8 @@ from re import match
|
||||||
|
|
||||||
from pyicloud.exceptions import (
|
from pyicloud.exceptions import (
|
||||||
PyiCloudFailedLoginException,
|
PyiCloudFailedLoginException,
|
||||||
PyiCloudAPIResponseError
|
PyiCloudAPIResponseError,
|
||||||
|
PyiCloud2FARequiredError
|
||||||
)
|
)
|
||||||
from pyicloud.services import (
|
from pyicloud.services import (
|
||||||
FindMyiPhoneServiceManager,
|
FindMyiPhoneServiceManager,
|
||||||
|
@ -46,7 +47,8 @@ class PyiCloudPasswordFilter(logging.Filter):
|
||||||
|
|
||||||
|
|
||||||
class PyiCloudSession(requests.Session):
|
class PyiCloudSession(requests.Session):
|
||||||
def __init__(self):
|
def __init__(self, service):
|
||||||
|
self.service = service
|
||||||
super(PyiCloudSession, self).__init__()
|
super(PyiCloudSession, self).__init__()
|
||||||
|
|
||||||
def request(self, *args, **kwargs):
|
def request(self, *args, **kwargs):
|
||||||
|
@ -76,6 +78,10 @@ class PyiCloudSession(requests.Session):
|
||||||
code = json.get('errorCode')
|
code = json.get('errorCode')
|
||||||
|
|
||||||
if reason:
|
if reason:
|
||||||
|
if self.service.requires_2fa and \
|
||||||
|
reason == 'Missing X-APPLE-WEBAUTH-TOKEN cookie':
|
||||||
|
raise PyiCloud2FARequiredError(response.url)
|
||||||
|
|
||||||
api_error = PyiCloudAPIResponseError(reason, code)
|
api_error = PyiCloudAPIResponseError(reason, code)
|
||||||
logger.error(api_error)
|
logger.error(api_error)
|
||||||
raise api_error
|
raise api_error
|
||||||
|
@ -99,7 +105,7 @@ class PyiCloudService(object):
|
||||||
if password is None:
|
if password is None:
|
||||||
password = get_password_from_keyring(apple_id)
|
password = get_password_from_keyring(apple_id)
|
||||||
|
|
||||||
self.discovery = None
|
self.data = {}
|
||||||
self.client_id = str(uuid.uuid1()).upper()
|
self.client_id = str(uuid.uuid1()).upper()
|
||||||
self.user = {'apple_id': apple_id, 'password': password}
|
self.user = {'apple_id': apple_id, 'password': password}
|
||||||
logger.addFilter(PyiCloudPasswordFilter(password))
|
logger.addFilter(PyiCloudPasswordFilter(password))
|
||||||
|
@ -119,7 +125,7 @@ class PyiCloudService(object):
|
||||||
'pyicloud',
|
'pyicloud',
|
||||||
)
|
)
|
||||||
|
|
||||||
self.session = PyiCloudSession()
|
self.session = PyiCloudSession(self)
|
||||||
self.session.verify = verify
|
self.session.verify = verify
|
||||||
self.session.headers.update({
|
self.session.headers.update({
|
||||||
'Origin': self._home_endpoint,
|
'Origin': self._home_endpoint,
|
||||||
|
@ -173,8 +179,8 @@ class PyiCloudService(object):
|
||||||
os.mkdir(self._cookie_directory)
|
os.mkdir(self._cookie_directory)
|
||||||
self.session.cookies.save()
|
self.session.cookies.save()
|
||||||
|
|
||||||
self.discovery = resp
|
self.data = resp
|
||||||
self.webservices = self.discovery['webservices']
|
self.webservices = self.data['webservices']
|
||||||
|
|
||||||
logger.info("Authentication completed successfully")
|
logger.info("Authentication completed successfully")
|
||||||
logger.debug(self.params)
|
logger.debug(self.params)
|
||||||
|
@ -186,6 +192,56 @@ class PyiCloudService(object):
|
||||||
''.join([c for c in self.user.get('apple_id') if match(r'\w', c)])
|
''.join([c for c in self.user.get('apple_id') if match(r'\w', c)])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def requires_2fa(self):
|
||||||
|
""" Returns True if two-factor authentication is required."""
|
||||||
|
return self.data.get('hsaChallengeRequired', False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def trusted_devices(self):
|
||||||
|
""" Returns devices trusted for two-factor authentication."""
|
||||||
|
request = self.session.get(
|
||||||
|
'%s/listDevices' % self._setup_endpoint,
|
||||||
|
params=self.params
|
||||||
|
)
|
||||||
|
return request.json().get('devices')
|
||||||
|
|
||||||
|
def send_verification_code(self, device):
|
||||||
|
""" Requests that a verification code is sent to the given device"""
|
||||||
|
data = json.dumps(device)
|
||||||
|
request = self.session.post(
|
||||||
|
'%s/sendVerificationCode' % self._setup_endpoint,
|
||||||
|
params=self.params,
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
return request.json().get('success', False)
|
||||||
|
|
||||||
|
def validate_verification_code(self, device, code):
|
||||||
|
""" Verifies a verification code received on a two-factor device"""
|
||||||
|
device.update({
|
||||||
|
'verificationCode': code,
|
||||||
|
'trustBrowser': True
|
||||||
|
})
|
||||||
|
data = json.dumps(device)
|
||||||
|
|
||||||
|
try:
|
||||||
|
request = self.session.post(
|
||||||
|
'%s/validateVerificationCode' % self._setup_endpoint,
|
||||||
|
params=self.params,
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
except PyiCloudAPIResponseError, error:
|
||||||
|
if error.code == -21669:
|
||||||
|
# Wrong verification code
|
||||||
|
return False
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Re-authenticate, which will both update the 2FA data, and
|
||||||
|
# ensure that we save the X-APPLE-WEBAUTH-HSA-TRUST cookie.
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
return not self.requires_2fa
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def devices(self):
|
def devices(self):
|
||||||
""" Return all devices."""
|
""" Return all devices."""
|
||||||
|
|
|
@ -22,5 +22,15 @@ class PyiCloudFailedLoginException(PyiCloudException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PyiCloud2FARequiredError(PyiCloudException):
|
||||||
|
def __init__(self, url):
|
||||||
|
message = "Two-factor authentication required for %s" % url
|
||||||
|
super(PyiCloud2FARequiredError, self).__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class PyiCloudNoDevicesException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NoStoredPasswordAvailable(PyiCloudException):
|
class NoStoredPasswordAvailable(PyiCloudException):
|
||||||
pass
|
pass
|
||||||
|
|
Loading…
Reference in a new issue