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.
|
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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -33,4 +33,7 @@ pycryptodome
|
||||||
phpserialize
|
phpserialize
|
||||||
dkimpy
|
dkimpy
|
||||||
pyotp
|
pyotp
|
||||||
flask_profiler
|
flask_profiler
|
||||||
|
facebook-sdk
|
||||||
|
google-api-python-client
|
||||||
|
google-auth-httplib2
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue