Merge pull request #95 from simple-login/api-social-login
Add API endpoints for Facebook & Google login
This commit is contained in:
commit
db621af1e5
17
README.md
17
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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -34,3 +34,6 @@ phpserialize
|
|||
dkimpy
|
||||
pyotp
|
||||
flask_profiler
|
||||
facebook-sdk
|
||||
google-api-python-client
|
||||
google-auth-httplib2
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue