diff --git a/README.md b/README.md index b2cfdc0c..4d51f2e8 100644 --- a/README.md +++ b/README.md @@ -738,6 +738,23 @@ Output: The `api_key` is used in all subsequent requests. It's empty if MFA is enabled. If user hasn't enabled MFA, `mfa_key` is empty. +#### POST /api/auth/facebook + +Input: +- facebook_token: Facebook access token +- device: device name. Used to create the API Key. Should be humanly readable so user can manage later on the "API Key" page. + +Output: Same output as for `/api/auth/login` endpoint + + +#### POST /api/auth/google + +Input: +- google_token: Facebook access token +- device: device name. Used to create the API Key. Should be humanly readable so user can manage later on the "API Key" page. + +Output: Same output as for `/api/auth/login` endpoint + #### GET /api/aliases Get user aliases. diff --git a/app/api/views/auth_login.py b/app/api/views/auth_login.py index 59e78c1c..cd29e918 100644 --- a/app/api/views/auth_login.py +++ b/app/api/views/auth_login.py @@ -1,14 +1,21 @@ -from flask import g +from flask import jsonify, request +import facebook +import google.oauth2.credentials +import googleapiclient.discovery from flask import jsonify, request from flask_cors import cross_origin from itsdangerous import Signer -from app.api.base import api_bp, verify_api_key -from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN, FLASK_SECRET +from app import email_utils +from app.api.base import api_bp +from app.config import ( + FLASK_SECRET, + DISABLE_REGISTRATION, +) +from app.email_utils import can_be_used_as_personal_email, email_already_used from app.extensions import db from app.log import LOG -from app.models import GenEmail, AliasUsedOn, User, ApiKey -from app.utils import convert_to_id +from app.models import User, ApiKey, SocialAuth @api_bp.route("/auth/login", methods=["POST"]) @@ -48,6 +55,107 @@ def auth_login(): return jsonify(**auth_payload(user, device)), 200 +@api_bp.route("/auth/facebook", methods=["POST"]) +@cross_origin() +def auth_facebook(): + """ + Authenticate user with Facebook + Input: + facebook_token: facebook access token + device: to create an ApiKey associated with this device + Output: + 200 and user info containing: + { + name: "John Wick", + mfa_enabled: true, + mfa_key: "a long string", + api_key: "a long string" + } + + """ + data = request.get_json() + if not data: + return jsonify(error="request body cannot be empty"), 400 + + facebook_token = data.get("facebook_token") + device = data.get("device") + + graph = facebook.GraphAPI(access_token=facebook_token) + user_info = graph.get_object("me", fields="email,name") + email = user_info.get("email") + + user = User.get_by(email=email) + + if not user: + if DISABLE_REGISTRATION: + return jsonify(error="registration is closed"), 400 + if not can_be_used_as_personal_email(email) or email_already_used(email): + return jsonify(error=f"cannot use {email} as personal inbox"), 400 + + LOG.d("create facebook user with %s", user_info) + user = User.create(email=email.lower(), name=user_info["name"], activated=True) + db.session.commit() + email_utils.send_welcome_email(user) + + if not SocialAuth.get_by(user_id=user.id, social="facebook"): + SocialAuth.create(user_id=user.id, social="facebook") + db.session.commit() + + return jsonify(**auth_payload(user, device)), 200 + + +@api_bp.route("/auth/google", methods=["POST"]) +@cross_origin() +def auth_google(): + """ + Authenticate user with Facebook + Input: + google_token: Google access token + device: to create an ApiKey associated with this device + Output: + 200 and user info containing: + { + name: "John Wick", + mfa_enabled: true, + mfa_key: "a long string", + api_key: "a long string" + } + + """ + data = request.get_json() + if not data: + return jsonify(error="request body cannot be empty"), 400 + + google_token = data.get("google_token") + device = data.get("device") + + cred = google.oauth2.credentials.Credentials(token=google_token) + + build = googleapiclient.discovery.build("oauth2", "v2", credentials=cred) + + user_info = build.userinfo().get().execute() + email = user_info.get("email") + + user = User.get_by(email=email) + + if not user: + if DISABLE_REGISTRATION: + return jsonify(error="registration is closed"), 400 + if not can_be_used_as_personal_email(email) or email_already_used(email): + return jsonify(error=f"cannot use {email} as personal inbox"), 400 + + LOG.d("create Google user with %s", user_info) + user = User.create(email=email.lower(), name="", activated=True) + db.session.commit() + email_utils.send_welcome_email(user) + + if not SocialAuth.get_by(user_id=user.id, social="google"): + SocialAuth.create(user_id=user.id, social="google") + db.session.commit() + + return jsonify(**auth_payload(user, device)), 200 + + def auth_payload(user, device) -> dict: ret = { "name": user.name, diff --git a/requirements.in b/requirements.in index 7cfc98ac..6eabfe21 100644 --- a/requirements.in +++ b/requirements.in @@ -33,4 +33,7 @@ pycryptodome phpserialize dkimpy pyotp -flask_profiler \ No newline at end of file +flask_profiler +facebook-sdk +google-api-python-client +google-auth-httplib2 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 68558ed4..2db4228d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,54 +5,60 @@ # pip-compile # aiohttp==3.5.4 # via raven-aiohttp, yacron -aiosmtpd==1.2 +aiosmtpd==1.2 # via -r requirements.in aiosmtplib==1.0.6 # via yacron alembic==1.0.10 # via flask-migrate appnope==0.1.0 # via ipython -arrow==0.14.2 +arrow==0.14.2 # via -r requirements.in asn1crypto==0.24.0 # via cryptography async-timeout==3.0.1 # via aiohttp atomicwrites==1.3.0 # via pytest atpublic==1.0 # via aiosmtpd attrs==19.1.0 # via aiohttp, pytest backcall==0.1.0 # via ipython -bcrypt==3.1.6 -blinker==1.4 -boto3==1.9.167 +bcrypt==3.1.6 # via -r requirements.in +blinker==1.4 # via -r requirements.in, flask-debugtoolbar +boto3==1.9.167 # via -r requirements.in, watchtower botocore==1.12.167 # via boto3, s3transfer +cachetools==4.0.0 # via google-auth certifi==2019.3.9 # via requests, sentry-sdk cffi==1.12.3 # via bcrypt, cryptography chardet==3.0.4 # via aiohttp, requests click==7.0 # via flask, pip-tools -coloredlogs==10.0 +coloredlogs==10.0 # via -r requirements.in crontab==0.22.5 # via yacron cryptography==2.7 # via jwcrypto, pyopenssl decorator==4.4.0 # via ipython, traitlets -dkimpy==1.0.1 -dnspython==1.16.0 +dkimpy==1.0.1 # via -r requirements.in +dnspython==1.16.0 # via -r requirements.in, dkimpy docutils==0.14 # via botocore -flask-admin==1.5.3 -flask-cors==3.0.8 -flask-debugtoolbar==0.10.1 +facebook-sdk==3.1.0 # via -r requirements.in +flask-admin==1.5.3 # via -r requirements.in +flask-cors==3.0.8 # via -r requirements.in +flask-debugtoolbar==0.10.1 # via -r requirements.in flask-httpauth==3.3.0 # via flask-profiler -flask-login==0.4.1 -flask-migrate==2.5.2 -flask-profiler==1.8.1 -flask-sqlalchemy==2.4.0 -flask-wtf==0.14.2 -flask==1.0.3 -gunicorn==19.9.0 +flask-login==0.4.1 # via -r requirements.in +flask-migrate==2.5.2 # via -r requirements.in +flask-profiler==1.8.1 # via -r requirements.in +flask-sqlalchemy==2.4.0 # via -r requirements.in, flask-migrate +flask-wtf==0.14.2 # via -r requirements.in +flask==1.0.3 # via -r requirements.in, flask-admin, flask-cors, flask-debugtoolbar, flask-httpauth, flask-login, flask-migrate, flask-profiler, flask-sqlalchemy, flask-wtf +google-api-python-client==1.7.11 # via -r requirements.in +google-auth-httplib2==0.0.3 # via -r requirements.in, google-api-python-client +google-auth==1.11.2 # via google-api-python-client, google-auth-httplib2 +gunicorn==19.9.0 # via -r requirements.in +httplib2==0.17.0 # via google-api-python-client, google-auth-httplib2 humanfriendly==4.18 # via coloredlogs idna-ssl==1.1.0 # via aiohttp idna==2.8 # via idna-ssl, requests, yarl importlib-metadata==0.18 # via pluggy, pytest ipython-genutils==0.2.0 # via traitlets -ipython==7.5.0 +ipython==7.5.0 # via -r requirements.in itsdangerous==1.1.0 # via flask, flask-debugtoolbar jedi==0.13.3 # via ipython jinja2==2.10.1 # via flask, yacron jmespath==0.9.4 # via boto3, botocore -jwcrypto==0.6.0 +jwcrypto==0.6.0 # via -r requirements.in mako==1.0.12 # via alembic markupsafe==1.1.1 # via jinja2, mako more-itertools==7.0.0 # via pytest @@ -61,47 +67,51 @@ oauthlib==3.0.2 # via requests-oauthlib packaging==19.0 # via pytest parso==0.4.0 # via jedi pexpect==4.7.0 # via ipython -phpserialize==1.3 +phpserialize==1.3 # via -r requirements.in pickleshare==0.7.5 # via ipython -pip-tools==3.8.0 +pip-tools==3.8.0 # via -r requirements.in pluggy==0.12.0 # via pytest prompt-toolkit==2.0.9 # via ipython -psycopg2-binary==2.8.2 +psycopg2-binary==2.8.2 # via -r requirements.in ptyprocess==0.6.0 # via pexpect py==1.8.0 # via pytest +pyasn1-modules==0.2.8 # via google-auth +pyasn1==0.4.8 # via pyasn1-modules, rsa pycparser==2.19 # via cffi -pycryptodome==3.9.4 +pycryptodome==3.9.4 # via -r requirements.in pygments==2.4.2 # via ipython -pyopenssl==19.0.0 -pyotp==2.3.0 +pyopenssl==19.0.0 # via -r requirements.in +pyotp==2.3.0 # via -r requirements.in pyparsing==2.4.0 # via packaging -pytest==4.6.3 +pytest==4.6.3 # via -r requirements.in python-dateutil==2.8.0 # via alembic, arrow, botocore, strictyaml -python-dotenv==0.10.3 +python-dotenv==0.10.3 # via -r requirements.in python-editor==1.0.4 # via alembic raven-aiohttp==0.7.0 # via yacron raven==6.10.0 # via raven-aiohttp, yacron -requests-oauthlib==1.2.0 -requests==2.22.0 # via requests-oauthlib +requests-oauthlib==1.2.0 # via -r requirements.in +requests==2.22.0 # via facebook-sdk, requests-oauthlib +rsa==4.0 # via google-auth ruamel.yaml==0.15.97 # via strictyaml s3transfer==0.2.1 # via boto3 -sentry-sdk==0.14.1 +sentry-sdk==0.14.1 # via -r requirements.in simplejson==3.17.0 # via flask-profiler -six==1.12.0 # via bcrypt, cryptography, flask-cors, packaging, pip-tools, prompt-toolkit, pyopenssl, pytest, python-dateutil, sqlalchemy-utils, traitlets -sqlalchemy-utils==0.36.1 +six==1.12.0 # via bcrypt, cryptography, flask-cors, google-api-python-client, google-auth, packaging, pip-tools, prompt-toolkit, pyopenssl, pytest, python-dateutil, sqlalchemy-utils, traitlets +sqlalchemy-utils==0.36.1 # via -r requirements.in sqlalchemy==1.3.12 # via alembic, flask-sqlalchemy, sqlalchemy-utils strictyaml==1.0.2 # via yacron traitlets==4.3.2 # via ipython typing-extensions==3.7.4.1 # via aiohttp -unidecode==1.0.23 +unidecode==1.0.23 # via -r requirements.in +uritemplate==3.0.1 # via google-api-python-client urllib3==1.25.3 # via botocore, requests, sentry-sdk -watchtower==0.6.0 +watchtower==0.6.0 # via -r requirements.in wcwidth==0.1.7 # via prompt-toolkit, pytest werkzeug==0.15.4 # via flask, flask-debugtoolbar -wtforms==2.2.1 -yacron==0.9.0 +wtforms==2.2.1 # via -r requirements.in, flask-admin, flask-wtf +yacron==0.9.0 # via -r requirements.in yarl==1.3.0 # via aiohttp zipp==0.5.1 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -# setuptools==45.2.0 # via ipython +# setuptools