Skip to content

Commit

Permalink
Nylas auth refactor (#27)
Browse files Browse the repository at this point in the history
- Refactored and simplified auth handlers (inbox/auth/*)
    - Supported providers are "custom", "gmail", "outlook", no more complex module registering logic.
    - Only one token per account (we're assuming the user doesn't have different tokens for email, calendar, contacts)
    - No longer using `GmailAuthCredentials` table. We only support one app token. However, for compatibility with old code, this table is still maintained.
    - By default, store the client ID on the account table to still allow migrating accounts from one client ID to another (This would require some minor work to feed multiple client secrets via config).
    - Keeping the account verification methods due to many tests using them, but we aren't using them so far.
- API (inbox/api/srv.py) and auth handler now uses attr objects instead of a dict. Supports both token and AuthAlligator auth info.
- Single entry point to obtain an access token via existing `TokenManager` which can use a refresh token or AuthAlligator (not implemented yet).
    - Email uses `get_authenticated_imap_connection` for convenience.
    - Events uses existing requests client with token from the token manager.
    - Contacts uses existing Google library but doesn't touch the refresh token since it uses `gdata.gauth.AuthSubToken` with the token from the token manager.
  • Loading branch information
thomasst authored Sep 22, 2020
1 parent 9e41c65 commit 16cbcc1
Show file tree
Hide file tree
Showing 42 changed files with 918 additions and 2,363 deletions.
9 changes: 3 additions & 6 deletions bin/inbox-auth
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ def main(email_address, reauth, target, provider):
if account is not None and not reauth:
sys.exit('Already have this account!')

auth_info = {}

if not provider:
provider = provider_from_address(email_address)

Expand All @@ -57,14 +55,13 @@ def main(email_address, reauth, target, provider):
is_imap = raw_input('IMAP account? [Y/n] ').strip().lower() != 'n'
provider = 'custom' if is_imap else 'eas'

auth_info['provider'] = provider
auth_handler = handler_from_provider(provider)
auth_info.update(auth_handler.interactive_auth(email_address))
account_data = auth_handler.interactive_auth(email_address)

if reauth:
account = auth_handler.update_account(account, auth_info)
account = auth_handler.update_account(account, account_data)
else:
account = auth_handler.create_account(email_address, auth_info)
account = auth_handler.create_account(account_data)

try:
if auth_handler.verify_account(account):
Expand Down
165 changes: 62 additions & 103 deletions inbox/api/srv.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
strict_parse_args,
valid_public_id,
)
from inbox.auth.generic import GenericAuthHandler
from inbox.auth.gmail import GmailAuthHandler
from inbox.auth.generic import GenericAccountData, GenericAuthHandler
from inbox.auth.google import GoogleAccountData, GoogleAuthHandler
from inbox.models import Account, Namespace
from inbox.models.backends.generic import GenericAccount
from inbox.models.backends.gmail import GOOGLE_EMAIL_SCOPE, GmailAccount
from inbox.models.secret import SecretType
from inbox.models.session import global_session_scope
from inbox.util.logging_helper import reconfigure_logging
from inbox.webhooks.gpush_notifications import app as webhooks_api
Expand Down Expand Up @@ -146,61 +147,72 @@ def ns_all():
return encoder.jsonify(namespaces)


@app.route("/accounts/", methods=["POST"])
def create_account():
""" Create a new account """
data = request.get_json(force=True)
def _get_account_data_for_generic_account(data):
email_address = data["email_address"]
sync_email = data.get("sync_email", True)
return GenericAccountData(
email=email_address,
imap_server_host=data["imap_server_host"],
imap_server_port=data["imap_server_port"],
imap_username=data["imap_username"],
imap_password=data["imap_password"],
smtp_server_host="localhost",
smtp_server_port=25,
smtp_username="dummy",
smtp_password="dummy",
sync_email=sync_email,
)

provider = data.get("provider", "custom")

def _get_account_data_for_google_account(data):
email_address = data["email_address"]
scopes = data.get("scopes", GOOGLE_EMAIL_SCOPE)
client_id = data.get("client_id")

sync_email = data.get("sync_email", True)
sync_calendar = data.get("sync_calendar", False)
sync_contacts = data.get("sync_contacts", False)

if data["type"] == "generic":
auth_handler = GenericAuthHandler(provider)
account = auth_handler.create_account(
email_address,
{
"name": "",
"email": email_address,
"imap_server_host": data["imap_server_host"],
"imap_server_port": data["imap_server_port"],
"imap_username": data["imap_username"],
"imap_password": data["imap_password"],
# Make Nylas happy with dummy values
"smtp_server_host": "localhost",
"smtp_server_port": 25,
"smtp_username": "dummy",
"smtp_password": "dummy",
"sync_email": sync_email,
},
)
refresh_token = data.get("refresh_token")
authalligator = data.get("authalligator")

if authalligator:
secret_type = SecretType.AuthAlligator
secret_value = authalligator
elif refresh_token:
secret_type = SecretType.Token
secret_value = refresh_token
else:
raise InputError("Authentication information missing.")

return GoogleAccountData(
email=email_address,
secret_type=secret_type,
secret_value=secret_value,
client_id=client_id,
scope=scopes,
sync_email=sync_email,
sync_events=sync_calendar,
sync_contacts=sync_contacts,
)

elif data["type"] == "gmail":
scopes = data.get("scopes", GOOGLE_EMAIL_SCOPE)
auth_handler = GmailAuthHandler(provider)
account = auth_handler.create_account(
email_address,
{
"name": "",
"email": email_address,
"refresh_token": data["refresh_token"],
"scope": scopes,
"id_token": "",
"contacts": False,
"sync_email": sync_email,
"events": sync_calendar,
},
)

@app.route("/accounts/", methods=["POST"])
def create_account():
""" Create a new account """
data = request.get_json(force=True)

if data["type"] == "generic":
auth_handler = GenericAuthHandler()
account_data = _get_account_data_for_generic_account(data)
elif data["type"] == "gmail":
auth_handler = GoogleAuthHandler()
account_data = _get_account_data_for_google_account(data)
else:
raise ValueError("Account type not supported.")

with global_session_scope() as db_session:
# By default, don't enable accounts so we have the ability to set a
# custom sync host.
account.sync_should_run = False
account = auth_handler.create_account(account_data)
db_session.add(account)
db_session.commit()

Expand All @@ -218,12 +230,6 @@ def modify_account(namespace_public_id):

data = request.get_json(force=True)

provider = data.get("provider", "custom")
email_address = data["email_address"]

sync_email = data.get("sync_email", True)
sync_calendar = data.get("sync_calendar", False)

with global_session_scope() as db_session:
namespace = (
db_session.query(Namespace)
Expand All @@ -233,62 +239,15 @@ def modify_account(namespace_public_id):
account = namespace.account

if isinstance(account, GenericAccount):
if "refresh_token" in data:
raise InputError(
"Cannot change the refresh token on a password account."
)

auth_handler = GenericAuthHandler(provider)
auth_handler.update_account(
account,
{
"name": "",
"email": email_address,
"imap_server_host": data["imap_server_host"],
"imap_server_port": data["imap_server_port"],
"imap_username": data["imap_username"],
"imap_password": data["imap_password"],
# Make Nylas happy with dummy values
"smtp_server_host": "localhost",
"smtp_server_port": 25,
"smtp_username": "dummy",
"smtp_password": "dummy",
"sync_email": sync_email,
},
)

auth_handler = GenericAuthHandler()
account_data = _get_account_data_for_generic_account(data)
elif isinstance(account, GmailAccount):
scopes = data.get("scopes", GOOGLE_EMAIL_SCOPE)
auth_handler = GmailAuthHandler(provider)
if "refresh_token" in data:
account = auth_handler.update_account(
account,
{
"name": "",
"email": email_address,
"refresh_token": data["refresh_token"],
"scope": scopes,
"id_token": "",
"sync_email": sync_email,
"contacts": False,
"events": sync_calendar,
},
)
else:
if (
"imap_server_host" in data
or "imap_server_port" in data
or "imap_username" in data
or "imap_password" in data
):
raise InputError("Cannot change IMAP fields on a Gmail account.")

auth_handler = GoogleAuthHandler()
account_data = _get_account_data_for_google_account(data)
else:
raise ValueError("Account type not supported.")

# By default, don't enable accounts so we have the ability to set a
# custom sync host.
account.disable_sync("modified-account")
account = auth_handler.update_account(account, account_data)
db_session.add(account)
db_session.commit()

Expand Down
19 changes: 0 additions & 19 deletions inbox/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +0,0 @@
"""
Per-provider auth modules.
An auth module *must* meet the following requirement:
1. Specify the provider it implements as the module-level PROVIDER variable.
For example, 'gmail', 'imap', 'eas', 'yahoo' etc.
2. Live in the 'auth/' directory.
3. Register an AuthHandler class as an entry point in setup.py
"""
# Allow out-of-tree auth submodules.
from pkgutil import extend_path

from inbox.util.misc import register_backends

__path__ = extend_path(__path__, __name__)
module_registry = register_backends(__name__, __path__)
Loading

0 comments on commit 16cbcc1

Please sign in to comment.