mirror of
https://github.com/simple-login/app.git
synced 2025-02-20 22:02:54 +08:00
Create /api/auth/login
This commit is contained in:
parent
ebbbf0a9f8
commit
d1734c3cf9
4 changed files with 128 additions and 6 deletions
26
README.md
26
README.md
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
|
|
64
app/api/views/auth_login.py
Normal file
64
app/api/views/auth_login.py
Normal 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
|
42
tests/api/test_auth_login.py
Normal file
42
tests/api/test_auth_login.py
Normal 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"
|
Loading…
Reference in a new issue