Merge pull request #95 from simple-login/api-social-login

Add API endpoints for Facebook & Google login
This commit is contained in:
Son Nguyen Kim 2020-02-28 17:56:09 +07:00 committed by GitHub
commit db621af1e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 182 additions and 44 deletions

View file

@ -738,6 +738,23 @@ Output:
The `api_key` is used in all subsequent requests. It's empty if MFA is enabled. 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. 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 /api/aliases
Get user aliases. Get user aliases.

View file

@ -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 import jsonify, request
from flask_cors import cross_origin from flask_cors import cross_origin
from itsdangerous import Signer from itsdangerous import Signer
from app.api.base import api_bp, verify_api_key from app import email_utils
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN, FLASK_SECRET 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.extensions import db
from app.log import LOG from app.log import LOG
from app.models import GenEmail, AliasUsedOn, User, ApiKey from app.models import User, ApiKey, SocialAuth
from app.utils import convert_to_id
@api_bp.route("/auth/login", methods=["POST"]) @api_bp.route("/auth/login", methods=["POST"])
@ -48,6 +55,107 @@ def auth_login():
return jsonify(**auth_payload(user, device)), 200 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: def auth_payload(user, device) -> dict:
ret = { ret = {
"name": user.name, "name": user.name,

View file

@ -33,4 +33,7 @@ pycryptodome
phpserialize phpserialize
dkimpy dkimpy
pyotp pyotp
flask_profiler flask_profiler
facebook-sdk
google-api-python-client
google-auth-httplib2

View file

@ -5,54 +5,60 @@
# pip-compile # pip-compile
# #
aiohttp==3.5.4 # via raven-aiohttp, yacron aiohttp==3.5.4 # via raven-aiohttp, yacron
aiosmtpd==1.2 aiosmtpd==1.2 # via -r requirements.in
aiosmtplib==1.0.6 # via yacron aiosmtplib==1.0.6 # via yacron
alembic==1.0.10 # via flask-migrate alembic==1.0.10 # via flask-migrate
appnope==0.1.0 # via ipython appnope==0.1.0 # via ipython
arrow==0.14.2 arrow==0.14.2 # via -r requirements.in
asn1crypto==0.24.0 # via cryptography asn1crypto==0.24.0 # via cryptography
async-timeout==3.0.1 # via aiohttp async-timeout==3.0.1 # via aiohttp
atomicwrites==1.3.0 # via pytest atomicwrites==1.3.0 # via pytest
atpublic==1.0 # via aiosmtpd atpublic==1.0 # via aiosmtpd
attrs==19.1.0 # via aiohttp, pytest attrs==19.1.0 # via aiohttp, pytest
backcall==0.1.0 # via ipython backcall==0.1.0 # via ipython
bcrypt==3.1.6 bcrypt==3.1.6 # via -r requirements.in
blinker==1.4 blinker==1.4 # via -r requirements.in, flask-debugtoolbar
boto3==1.9.167 boto3==1.9.167 # via -r requirements.in, watchtower
botocore==1.12.167 # via boto3, s3transfer botocore==1.12.167 # via boto3, s3transfer
cachetools==4.0.0 # via google-auth
certifi==2019.3.9 # via requests, sentry-sdk certifi==2019.3.9 # via requests, sentry-sdk
cffi==1.12.3 # via bcrypt, cryptography cffi==1.12.3 # via bcrypt, cryptography
chardet==3.0.4 # via aiohttp, requests chardet==3.0.4 # via aiohttp, requests
click==7.0 # via flask, pip-tools click==7.0 # via flask, pip-tools
coloredlogs==10.0 coloredlogs==10.0 # via -r requirements.in
crontab==0.22.5 # via yacron crontab==0.22.5 # via yacron
cryptography==2.7 # via jwcrypto, pyopenssl cryptography==2.7 # via jwcrypto, pyopenssl
decorator==4.4.0 # via ipython, traitlets decorator==4.4.0 # via ipython, traitlets
dkimpy==1.0.1 dkimpy==1.0.1 # via -r requirements.in
dnspython==1.16.0 dnspython==1.16.0 # via -r requirements.in, dkimpy
docutils==0.14 # via botocore docutils==0.14 # via botocore
flask-admin==1.5.3 facebook-sdk==3.1.0 # via -r requirements.in
flask-cors==3.0.8 flask-admin==1.5.3 # via -r requirements.in
flask-debugtoolbar==0.10.1 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-httpauth==3.3.0 # via flask-profiler
flask-login==0.4.1 flask-login==0.4.1 # via -r requirements.in
flask-migrate==2.5.2 flask-migrate==2.5.2 # via -r requirements.in
flask-profiler==1.8.1 flask-profiler==1.8.1 # via -r requirements.in
flask-sqlalchemy==2.4.0 flask-sqlalchemy==2.4.0 # via -r requirements.in, flask-migrate
flask-wtf==0.14.2 flask-wtf==0.14.2 # via -r requirements.in
flask==1.0.3 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
gunicorn==19.9.0 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 humanfriendly==4.18 # via coloredlogs
idna-ssl==1.1.0 # via aiohttp idna-ssl==1.1.0 # via aiohttp
idna==2.8 # via idna-ssl, requests, yarl idna==2.8 # via idna-ssl, requests, yarl
importlib-metadata==0.18 # via pluggy, pytest importlib-metadata==0.18 # via pluggy, pytest
ipython-genutils==0.2.0 # via traitlets 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 itsdangerous==1.1.0 # via flask, flask-debugtoolbar
jedi==0.13.3 # via ipython jedi==0.13.3 # via ipython
jinja2==2.10.1 # via flask, yacron jinja2==2.10.1 # via flask, yacron
jmespath==0.9.4 # via boto3, botocore 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 mako==1.0.12 # via alembic
markupsafe==1.1.1 # via jinja2, mako markupsafe==1.1.1 # via jinja2, mako
more-itertools==7.0.0 # via pytest more-itertools==7.0.0 # via pytest
@ -61,47 +67,51 @@ oauthlib==3.0.2 # via requests-oauthlib
packaging==19.0 # via pytest packaging==19.0 # via pytest
parso==0.4.0 # via jedi parso==0.4.0 # via jedi
pexpect==4.7.0 # via ipython pexpect==4.7.0 # via ipython
phpserialize==1.3 phpserialize==1.3 # via -r requirements.in
pickleshare==0.7.5 # via ipython 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 pluggy==0.12.0 # via pytest
prompt-toolkit==2.0.9 # via ipython 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 ptyprocess==0.6.0 # via pexpect
py==1.8.0 # via pytest 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 pycparser==2.19 # via cffi
pycryptodome==3.9.4 pycryptodome==3.9.4 # via -r requirements.in
pygments==2.4.2 # via ipython pygments==2.4.2 # via ipython
pyopenssl==19.0.0 pyopenssl==19.0.0 # via -r requirements.in
pyotp==2.3.0 pyotp==2.3.0 # via -r requirements.in
pyparsing==2.4.0 # via packaging 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-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 python-editor==1.0.4 # via alembic
raven-aiohttp==0.7.0 # via yacron raven-aiohttp==0.7.0 # via yacron
raven==6.10.0 # via raven-aiohttp, yacron raven==6.10.0 # via raven-aiohttp, yacron
requests-oauthlib==1.2.0 requests-oauthlib==1.2.0 # via -r requirements.in
requests==2.22.0 # via requests-oauthlib requests==2.22.0 # via facebook-sdk, requests-oauthlib
rsa==4.0 # via google-auth
ruamel.yaml==0.15.97 # via strictyaml ruamel.yaml==0.15.97 # via strictyaml
s3transfer==0.2.1 # via boto3 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 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 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 sqlalchemy-utils==0.36.1 # via -r requirements.in
sqlalchemy==1.3.12 # via alembic, flask-sqlalchemy, sqlalchemy-utils sqlalchemy==1.3.12 # via alembic, flask-sqlalchemy, sqlalchemy-utils
strictyaml==1.0.2 # via yacron strictyaml==1.0.2 # via yacron
traitlets==4.3.2 # via ipython traitlets==4.3.2 # via ipython
typing-extensions==3.7.4.1 # via aiohttp 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 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 wcwidth==0.1.7 # via prompt-toolkit, pytest
werkzeug==0.15.4 # via flask, flask-debugtoolbar werkzeug==0.15.4 # via flask, flask-debugtoolbar
wtforms==2.2.1 wtforms==2.2.1 # via -r requirements.in, flask-admin, flask-wtf
yacron==0.9.0 yacron==0.9.0 # via -r requirements.in
yarl==1.3.0 # via aiohttp yarl==1.3.0 # via aiohttp
zipp==0.5.1 # via importlib-metadata zipp==0.5.1 # via importlib-metadata
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:
# setuptools==45.2.0 # via ipython # setuptools