Skip to content

Commit

Permalink
Add support for two-factor authentication
Browse files Browse the repository at this point in the history
When 2FA is enabled in iCloud most iCloud services are unavailable
without first going through the 2FA handshake. We now have API to
initiate the 2FA, which can be used by more advanced API clients.

The built in command line 'icloud' application has not been updated,
as listing and managing devices though Find my iPhone is one of the
services that do not require 2FA.

Fixes issue #66.
  • Loading branch information
torarnv committed Feb 25, 2016
1 parent c3da622 commit 7ec72a1
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 6 deletions.
30 changes: 30 additions & 0 deletions README.rst
Expand Up @@ -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

*******************************
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
=======
Expand Down
68 changes: 62 additions & 6 deletions pyicloud/base.py
Expand Up @@ -12,7 +12,8 @@

from pyicloud.exceptions import (
PyiCloudFailedLoginException,
PyiCloudAPIResponseError
PyiCloudAPIResponseError,
PyiCloud2FARequiredError
)
from pyicloud.services import (
FindMyiPhoneServiceManager,
Expand Down Expand Up @@ -46,7 +47,8 @@ def filter(self, record):


class PyiCloudSession(requests.Session):
def __init__(self):
def __init__(self, service):
self.service = service
super(PyiCloudSession, self).__init__()

def request(self, *args, **kwargs):
Expand Down Expand Up @@ -76,6 +78,10 @@ def request(self, *args, **kwargs):
code = json.get('errorCode')

if reason:
if self.service.requires_2fa and \
reason == 'Missing X-APPLE-WEBAUTH-TOKEN cookie':
raise PyiCloud2FARequiredError(response.url)

api_error = PyiCloudAPIResponseError(reason, code)
logger.error(api_error)
raise api_error
Expand All @@ -99,7 +105,7 @@ def __init__(
if password is None:
password = get_password_from_keyring(apple_id)

self.discovery = None
self.data = {}
self.client_id = str(uuid.uuid1()).upper()
self.user = {'apple_id': apple_id, 'password': password}
logger.addFilter(PyiCloudPasswordFilter(password))
Expand All @@ -119,7 +125,7 @@ def __init__(
'pyicloud',
)

self.session = PyiCloudSession()
self.session = PyiCloudSession(self)
self.session.verify = verify
self.session.headers.update({
'Origin': self._home_endpoint,
Expand Down Expand Up @@ -173,8 +179,8 @@ def authenticate(self):
os.mkdir(self._cookie_directory)
self.session.cookies.save()

self.discovery = resp
self.webservices = self.discovery['webservices']
self.data = resp
self.webservices = self.data['webservices']

logger.info("Authentication completed successfully")
logger.debug(self.params)
Expand All @@ -186,6 +192,56 @@ def _get_cookiejar_path(self):
''.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
def devices(self):
""" Return all devices."""
Expand Down
10 changes: 10 additions & 0 deletions pyicloud/exceptions.py
Expand Up @@ -22,5 +22,15 @@ class PyiCloudFailedLoginException(PyiCloudException):
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):
pass

0 comments on commit 7ec72a1

Please sign in to comment.