Clarify that we support two-step, not two-factor, authentication

Two-step authentication is an older security method used for
accounts without an Apple device, or who are unable to upgrade
to iOS 9 or OS X El Capitan.

https://support.apple.com/en-us/HT204152

If the account has two-factor authentication enabled, we can still
fall back to the end-points for two-step authentication, as we do
not support 2FA yet.

Issue #102
This commit is contained in:
Tor Arne Vestbø 2017-01-09 22:12:00 +01:00
parent be3d447c00
commit 69af919ad5
4 changed files with 49 additions and 19 deletions

View file

@ -36,17 +36,19 @@ 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
******************************* **Note**: Authentication will expire after an interval set by Apple, at which point you will have to re-authenticate. This interval is currently two months.
Two-factor authentication (2FA)
*******************************
If you have enabled two-factor authentication for the account you will have to do some extra work: ************************************************
Two-step and two-factor authentication (2SA/2FA)
************************************************
If you have enabled `two-step authentication (2SA) <https://support.apple.com/en-us/HT204152>`_ for the account you will have to do some extra work:
.. code-block:: python .. code-block:: python
if api.requires_2fa: if api.requires_2sa:
import click import click
print "Two-factor authentication required. Your trusted devices are:" print "Two-step authentication required. Your trusted devices are:"
devices = api.trusted_devices devices = api.trusted_devices
for i, device in enumerate(devices): for i, device in enumerate(devices):
@ -64,7 +66,8 @@ If you have enabled two-factor authentication for the account you will have to d
print "Failed to verify verification code" print "Failed to verify verification code"
sys.exit(1) 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. This approach also works if the account is set up for `two-factor authentication (2FA) <https://support.apple.com/en-us/HT204915>`_, but the authentication will time out after a few hours. Full support for two-factor authentication (2FA) is not implemented in PyiCloud yet. See issue `#102 <https://github.com/picklepete/pyicloud/issues/102>`_.
======= =======
Devices Devices

View file

@ -13,7 +13,7 @@ from re import match
from pyicloud.exceptions import ( from pyicloud.exceptions import (
PyiCloudFailedLoginException, PyiCloudFailedLoginException,
PyiCloudAPIResponseError, PyiCloudAPIResponseError,
PyiCloud2FARequiredError PyiCloud2SARequiredError
) )
from pyicloud.services import ( from pyicloud.services import (
FindMyiPhoneServiceManager, FindMyiPhoneServiceManager,
@ -100,7 +100,7 @@ class PyiCloudSession(requests.Session):
def _raise_error(self, code, reason): def _raise_error(self, code, reason):
if self.service.requires_2fa and \ if self.service.requires_2fa and \
reason == 'Missing X-APPLE-WEBAUTH-TOKEN cookie': reason == 'Missing X-APPLE-WEBAUTH-TOKEN cookie':
raise PyiCloud2FARequiredError(response.url) raise PyiCloud2SARequiredError(response.url)
api_error = PyiCloudAPIResponseError(reason, code) api_error = PyiCloudAPIResponseError(reason, code)
logger.error(api_error) logger.error(api_error)
@ -217,13 +217,15 @@ class PyiCloudService(object):
) )
@property @property
def requires_2fa(self): def requires_2sa(self):
""" Returns True if two-factor authentication is required.""" """ Returns True if two-step authentication is required."""
return self.data.get('hsaChallengeRequired', False) return self.data.get('hsaChallengeRequired', False) \
and self.data['dsInfo'].get('hsaVersion', 0) >= 1
# FIXME: Implement 2FA for hsaVersion == 2
@property @property
def trusted_devices(self): def trusted_devices(self):
""" Returns devices trusted for two-factor authentication.""" """ Returns devices trusted for two-step authentication."""
request = self.session.get( request = self.session.get(
'%s/listDevices' % self._setup_endpoint, '%s/listDevices' % self._setup_endpoint,
params=self.params params=self.params
@ -241,7 +243,7 @@ class PyiCloudService(object):
return request.json().get('success', False) return request.json().get('success', False)
def validate_verification_code(self, device, code): def validate_verification_code(self, device, code):
""" Verifies a verification code received on a two-factor device""" """ Verifies a verification code received on a trusted device"""
device.update({ device.update({
'verificationCode': code, 'verificationCode': code,
'trustBrowser': True 'trustBrowser': True
@ -260,11 +262,11 @@ class PyiCloudService(object):
return False return False
raise raise
# Re-authenticate, which will both update the 2FA data, and # Re-authenticate, which will both update the HSA data, and
# ensure that we save the X-APPLE-WEBAUTH-HSA-TRUST cookie. # ensure that we save the X-APPLE-WEBAUTH-HSA-TRUST cookie.
self.authenticate() self.authenticate()
return not self.requires_2fa return not self.requires_2sa
@property @property
def devices(self): def devices(self):

View file

@ -207,6 +207,31 @@ def main(args=None):
confirm("Save password in keyring? ") confirm("Save password in keyring? ")
): ):
utils.store_password_in_keyring(username, password) utils.store_password_in_keyring(username, password)
if api.requires_2sa:
import click
print("Two-step authentication required.",
"Your trusted devices are:")
devices = api.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 api.send_verification_code(device):
print("Failed to send verification code")
sys.exit(1)
code = click.prompt('Please enter validation code')
if not api.validate_verification_code(device, code):
print("Failed to verify verification code")
sys.exit(1)
break break
except pyicloud.exceptions.PyiCloudFailedLoginException: except pyicloud.exceptions.PyiCloudFailedLoginException:
# If they have a stored password; we just used it and # If they have a stored password; we just used it and

View file

@ -22,10 +22,10 @@ class PyiCloudFailedLoginException(PyiCloudException):
pass pass
class PyiCloud2FARequiredError(PyiCloudException): class PyiCloud2SARequiredError(PyiCloudException):
def __init__(self, url): def __init__(self, url):
message = "Two-factor authentication required for %s" % url message = "Two-step authentication required for %s" % url
super(PyiCloud2FARequiredError, self).__init__(message) super(PyiCloud2SARequiredError, self).__init__(message)
class PyiCloudNoDevicesException(Exception): class PyiCloudNoDevicesException(Exception):