Skip to content

Commit

Permalink
Use AuthAlligator client to obtain access token (#33)
Browse files Browse the repository at this point in the history
- Support to add/update AuthAlligator accounts via API.
- Added back some Gmail auth code that we previously removed (we currently have a few accounts where auth fails retry forever), and also made it work for Microsoft accounts.
- Propagate the token manager's `force_refresh` parameter to AuthAlligator's verify method.

Example for adding an account:

```
curl -XPOST localhost:5555/accounts/ -d '{"type": "microsoft", "email_address":"[email protected]", "scopes": "the scopes", "authalligator": "{\"provider\": \"MICROSOFT\", \"accountKey\": \"abc123\", \"username\": \"00000000-0000-0000-xxxx-xxxxxxxxxxxx\"}", "client_id": "hello"}'
```
  • Loading branch information
thomasst authored Sep 30, 2020
1 parent 8391942 commit c689736
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 12 deletions.
4 changes: 3 additions & 1 deletion etc/config-dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
"MAILGUN_API_KEY": null,

"NOTIFICATIONS_MAILGUN_DOMAIN": null,
"NOTIFICATIONS_MAILGUN_API_KEY": null
"NOTIFICATIONS_MAILGUN_API_KEY": null,

"AUTHALLIGATOR_SERVICE_URL": "https://authalligator-service"

}
1 change: 1 addition & 0 deletions etc/secrets-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ GOOGLE_OAUTH_CLIENT_SECRET: zgY9wgwML0kmQ6mmYHYJE05d
MICROSOFT_OAUTH_CLIENT_ID: ms_oauth_client_id
MICROSOFT_OAUTH_CLIENT_SECRET: ms_oauth_client_secret
MICROSOFT_OAUTH_REDIRECT_URI: https://example.com/sandbox
AUTHALLIGATOR_AUTH_KEY: super-secret-authalligator-auth-key
# Hexl-encoded static keys used to encrypt blocks in S3, secrets in database:
BLOCK_ENCRYPTION_KEY: 43933ee4aff59913b7cd7204d87ee18cd5d0faea4df296cb7863f9f28525f7cd
SECRET_ENCRYPTION_KEY: 5f2356f7e2dfc4ccc93458d27147f97b954a56cc0554273cb6fee070cbadd050
Expand Down
1 change: 1 addition & 0 deletions etc/secrets-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ GOOGLE_OAUTH_CLIENT_SECRET: zgY9wgwML0kmQ6mmYHYJE05d
MICROSOFT_OAUTH_CLIENT_ID: ms_oauth_client_id
MICROSOFT_OAUTH_CLIENT_SECRET: ms_oauth_client_secret
MICROSOFT_OAUTH_REDIRECT_URI: https://example.com/sandbox
AUTHALLIGATOR_AUTH_KEY: super-secret-authalligator-auth-key

# Hexl-encoded static keys used to encrypt blocks in S3, secrets in database:
BLOCK_ENCRYPTION_KEY: 0ba4c7da83f474d2b33c8725416e444db632a1684705bc2fb7da5058e93668c9
Expand Down
130 changes: 122 additions & 8 deletions inbox/auth/oauth.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import datetime
import json

import pytz
import requests
from authalligator_client.client import Client as AuthAlligatorApiClient
from authalligator_client.enums import AccountErrorCode, ProviderType
from authalligator_client.exceptions import AccountError
from imapclient import IMAPClient
from nylas.logging import get_logger
from six.moves import urllib

from inbox.basicauth import ConnectionError, OAuthError
from inbox.basicauth import ConnectionError, ImapSupportDisabledError, OAuthError
from inbox.config import config
from inbox.models.backends.oauth import token_manager
from inbox.models.secret import SecretType

Expand All @@ -18,6 +24,9 @@ class OAuthAuthHandler(AuthHandler):
# Defined by subclasses
OAUTH_ACCESS_TOKEN_URL = None

AUTHALLIGATOR_AUTH_KEY = config.get("AUTHALLIGATOR_AUTH_KEY")
AUTHALLIGATOR_SERVICE_URL = config.get("AUTHALLIGATOR_SERVICE_URL")

def _new_access_token_from_refresh_token(self, account):
refresh_token = account.refresh_token
if not refresh_token:
Expand Down Expand Up @@ -67,16 +76,75 @@ def _new_access_token_from_refresh_token(self, account):

return session_dict["access_token"], session_dict["expires_in"]

def _access_token_from_authalligator(self, account):
def _new_access_token_from_authalligator(self, account, force_refresh):
"""
Return the access token based on an account created in AuthAlligator.
"""
assert account.secret.type == SecretType.AuthAlligator.value
# aa_data = json.loads(account.secret.secret)
# TODO: get verified token
raise NotImplementedError("Not implemented yet.")
assert self.AUTHALLIGATOR_AUTH_KEY
assert self.AUTHALLIGATOR_SERVICE_URL

aa_client = AuthAlligatorApiClient(
token=self.AUTHALLIGATOR_AUTH_KEY,
service_url=self.AUTHALLIGATOR_SERVICE_URL,
)
aa_data = json.loads(account.secret.secret)
provider = ProviderType(aa_data["provider"])
username = aa_data["username"]
account_key = aa_data["account_key"]

try:
if force_refresh:
aa_response = aa_client.verify_account(
provider=provider, username=username, account_key=account_key,
)
aa_account = aa_response.account
else:
aa_response = aa_client.query_account(
provider=provider, username=username, account_key=account_key,
)
aa_account = aa_response
except AccountError as exc:
log.warn(
"AccountError during AuthAlligator account query",
account_id=account.id,
error_code=exc.code and exc.code.value,
error_message=exc.message,
retry_in=exc.retry_in,
)
if exc.code in (
AccountErrorCode.AUTHORIZATION_ERROR,
AccountErrorCode.CONFIGURATION_ERROR,
AccountErrorCode.DOES_NOT_EXIST,
):
raise OAuthError("Could not obtain access token from AuthAlligator")
else:
raise ConnectionError(
"Temporary error while obtaining access token from AuthAlligator"
)
else:
now = datetime.datetime.now(pytz.UTC)
expires_in = int((aa_account.access_token_expires_at - now).total_seconds())
assert expires_in > 0
return (aa_account.access_token, expires_in)

def acquire_access_token(self, account, force_refresh=False):
"""
Acquire a new access token for the given account.
def acquire_access_token(self, account):
Args:
force_refresh (bool): Whether a token refresh should be forced when
requesting it from an external token service (AuthAlligator)
Raises:
OAuthError: If the token is no longer valid and syncing should stop.
ConnectionError: If there was a temporary/connection error renewing
the auth token.
"""
if account.secret.type == SecretType.AuthAlligator.value:
return self._new_access_token_from_authalligator(account)
return self._new_access_token_from_authalligator(account, force_refresh)
elif account.secret.type == SecretType.Token.value:
# Any token requested from the refresh token is refreshed already.
return self._new_access_token_from_refresh_token(account)
else:
raise OAuthError("No supported secret found.")
Expand All @@ -86,10 +154,38 @@ def authenticate_imap_connection(self, account, conn):
try:
conn.oauth2_login(account.email_address, token)
except IMAPClient.Error as exc:
exc = _process_imap_exception(exc)

# Raise all IMAP disabled errors except authentication_failed
# error, which we handle differently.
if (
isinstance(exc, ImapSupportDisabledError)
and exc.reason != "authentication_failed"
):
raise exc

log.error(
"Error during IMAP XOAUTH2 login", account_id=account.id, error=exc,
)
raise
if not isinstance(exc, ImapSupportDisabledError):
raise # Unknown IMAPClient error, reraise

# If we got an AUTHENTICATIONFAILED response, force a token refresh
# and try again. If IMAP auth still fails, it's likely that IMAP
# access is disabled, so propagate that errror.
token = token_manager.get_token(account, force_refresh=True)
try:
conn.oauth2_login(account.email_address, token)
except IMAPClient.Error as exc:
exc = _process_imap_exception(exc)
if (
not isinstance(exc, ImapSupportDisabledError)
or exc.reason != "authentication_failed"
):
raise exc
else:
# Instead of authentication_failed, report imap disabled
raise ImapSupportDisabledError("imap_disabled_for_account")

def _get_user_info(self, session_dict):
access_token = session_dict["access_token"]
Expand Down Expand Up @@ -150,3 +246,21 @@ def __init__(self, token):
def __call__(self, r):
r.headers["Authorization"] = "Bearer {}".format(self.token)
return r


def _process_imap_exception(exc):
if "Lookup failed" in exc.message:
# Gmail is disabled for this apps account
return ImapSupportDisabledError("gmail_disabled_for_domain")
elif "IMAP access is disabled for your domain." in exc.message:
# IMAP is disabled for this domain
return ImapSupportDisabledError("imap_disabled_for_domain")
elif exc.message.startswith("[AUTHENTICATIONFAILED] Invalid credentials (Failure)"):
# Google
return ImapSupportDisabledError("authentication_failed")
elif exc.message.startswith("AUTHENTICATE failed."):
# Microsoft
return ImapSupportDisabledError("authentication_failed")
else:
# Unknown IMAPClient error
return exc
12 changes: 9 additions & 3 deletions inbox/models/backends/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def get_token(self, account, force_refresh=False):
if not force_refresh and expiration > datetime.utcnow():
return token

new_token, expires_in = account.new_token()
new_token, expires_in = account.new_token(force_refresh=force_refresh)
self.cache_token(account, new_token, expires_in)
return new_token

Expand Down Expand Up @@ -91,21 +91,27 @@ def get_client_info(self):
else:
raise OAuthError("No valid tokens.")

def new_token(self):
def new_token(self, force_refresh=False):
"""
Retrieves a new access token.
Args:
force_refresh (bool): Whether a token refresh should be forced when
requesting it from an external token service (AuthAlligator)
Returns:
A tuple with the new access token and its expiration.
Raises:
OAuthError: If no token could be obtained.
"""
try:
return self.auth_handler.acquire_access_token(self)
return self.auth_handler.acquire_access_token(
self, force_refresh=force_refresh
)
except Exception as e:
log.error(
"Error while getting access token: {}".format(e),
force_refresh=force_refresh,
account_id=self.id,
exc_info=True,
)
Expand Down
1 change: 1 addition & 0 deletions requirements_frozen.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ arrow==0.5.4
asn1crypto==0.24.0
astroid==1.6.1
attrs==20.2.0
git+https://github.com/closeio/authalligator-client.git@58310ba34c639f4504dff8d9c64c0247d27100f8#egg=authalligator_client
backports.functools-lru-cache==1.4
backports.ssl==0.0.9
boto==2.10.0
Expand Down

0 comments on commit c689736

Please sign in to comment.