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:
parent
be3d447c00
commit
69af919ad5
4 changed files with 49 additions and 19 deletions
17
README.rst
17
README.rst
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in a new issue