Create /api/auth/login

This commit is contained in:
Son NK 2020-01-20 14:36:39 +01:00
parent ebbbf0a9f8
commit d1734c3cf9
4 changed files with 128 additions and 6 deletions

View File

@ -437,13 +437,13 @@ john@wick.com / password
### API
For now the only API client is the Chrome/Firefox extension. This extension relies on `API Code` for authentication.
SimpleLogin current API clients are Chrome/Firefox/Safari extension and mobile (iOS/Android) app.
These clients rely on `API Code` for authentication.
In every request, the extension sends
Once the `Api Code` is obtained, either via user entering it (in Browser extension case) or by logging in (in Mobile case),
the client includes the `api code` in `Authentication` header in almost all requests.
- the `API Code` is set in `Authentication` header. The check is done via the `verify_api_key` wrapper, implemented in `app/api/base.py`
- (Optional but recommended) `hostname` passed in query string. hostname is the the URL hostname (cf https://en.wikipedia.org/wiki/URL), for ex if URL is http://www.example.com/index.html then the hostname is `www.example.com`. This information is important to know where an alias is used in order to suggest user the same alias if they want to create on alias on the same website in the future.
For some endpoints, the `hostname` should be passed in query string. `hostname` is the the URL hostname (cf https://en.wikipedia.org/wiki/URL), for ex if URL is http://www.example.com/index.html then the hostname is `www.example.com`. This information is important to know where an alias is used in order to suggest user the same alias if they want to create on alias on the same website in the future.
If error, the API returns 4** with body containing the error message, for example:
@ -553,6 +553,22 @@ If success, 201 with the new alias, for example
}
```
#### POST /api/auth/login
Input:
- email
- password
- device: device name. Used to create the API Key. Should be humanly readable so user can manage later on the "API Key" page.
Output:
- name: user name, could be an empty string
- mfa_enabled: boolean
- mfa_key: only useful when user enables MFA. In this case, user needs to enter their OTP token in order to login.
- api_key: if MFA is not enabled, the `api key` is returned right away.
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.
### Database migration
The database migration is handled by `alembic`

View File

@ -1 +1 @@
from .views import alias_options, new_custom_alias, new_random_alias, user_info
from .views import alias_options, new_custom_alias, new_random_alias, user_info, auth_login

View File

@ -0,0 +1,64 @@
from flask import g
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.extensions import db
from app.log import LOG
from app.models import GenEmail, AliasUsedOn, User, ApiKey
from app.utils import convert_to_id
@api_bp.route("/auth/login", methods=["POST"])
@cross_origin()
def auth_login():
"""
Authenticate user
Input:
email
password
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
email = data.get("email")
password = data.get("password")
device = data.get("device")
user = User.filter_by(email=email).first()
if not user or not user.check_password(password):
return jsonify(error="Email or password incorrect"), 400
elif not user.activated:
return jsonify(error="Account not activated"), 400
ret = {
"name": user.name,
"mfa_enabled": user.enable_otp,
}
# do not give api_key, user can only obtain api_key after OTP verification
if user.enable_otp:
s = Signer(FLASK_SECRET)
ret["mfa_key"] = s.sign(str(user.id))
ret["api_key"] = ""
else:
api_key = ApiKey.create(user.id, device)
db.session.commit()
ret["mfa_key"] = ""
ret["api_key"] = api_key.code
return jsonify(**ret), 200

View File

@ -0,0 +1,42 @@
from flask import url_for
from app.extensions import db
from app.models import User
def test_auth_login_success_mfa_disabled(flask_client):
User.create(email="a@b.c", password="password", name="Test User", activated=True)
db.session.commit()
r = flask_client.post(
url_for("api.auth_login"),
json={"email": "a@b.c", "password": "password", "device": "Test Device"},
)
assert r.status_code == 200
assert r.json["api_key"]
assert r.json["mfa_enabled"] == False
assert r.json["mfa_key"] == ""
assert r.json["name"] == "Test User"
def test_auth_login_success_mfa_enabled(flask_client):
User.create(
email="a@b.c",
password="password",
name="Test User",
activated=True,
enable_otp=True,
)
db.session.commit()
r = flask_client.post(
url_for("api.auth_login"),
json={"email": "a@b.c", "password": "password", "device": "Test Device"},
)
assert r.status_code == 200
assert r.json["api_key"] == ""
assert r.json["mfa_enabled"] == True
assert r.json["mfa_key"]
assert r.json["name"] == "Test User"