mirror of
https://github.com/simple-login/app.git
synced 2024-09-27 20:31:30 +02:00
add /api/auth/mfa
This commit is contained in:
parent
d1734c3cf9
commit
ef788f7458
14
README.md
14
README.md
@ -569,6 +569,20 @@ 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/mfa
|
||||||
|
|
||||||
|
Input:
|
||||||
|
- mfa_token: OTP token that user enters
|
||||||
|
- mfa_key: MFA key obtained in previous auth request, e.g. /api/auth/login
|
||||||
|
- device: the device name, used to create an ApiKey associated with this device
|
||||||
|
|
||||||
|
Output:
|
||||||
|
- name: user name, could be an empty string
|
||||||
|
- 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
|
### Database migration
|
||||||
|
|
||||||
The database migration is handled by `alembic`
|
The database migration is handled by `alembic`
|
||||||
|
@ -1 +1,8 @@
|
|||||||
from .views import alias_options, new_custom_alias, new_random_alias, user_info, auth_login
|
from .views import (
|
||||||
|
alias_options,
|
||||||
|
new_custom_alias,
|
||||||
|
new_random_alias,
|
||||||
|
user_info,
|
||||||
|
auth_login,
|
||||||
|
auth_mfa,
|
||||||
|
)
|
||||||
|
65
app/api/views/auth_mfa.py
Normal file
65
app/api/views/auth_mfa.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import pyotp
|
||||||
|
from flask import jsonify, request
|
||||||
|
from flask_cors import cross_origin
|
||||||
|
from itsdangerous import Signer, BadSignature
|
||||||
|
|
||||||
|
from app.api.base import api_bp
|
||||||
|
from app.config import FLASK_SECRET
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models import User, ApiKey
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/auth/mfa", methods=["POST"])
|
||||||
|
@cross_origin()
|
||||||
|
def auth_mfa():
|
||||||
|
"""
|
||||||
|
Validate the OTP Token
|
||||||
|
Input:
|
||||||
|
mfa_token: OTP token that user enters
|
||||||
|
mfa_key: MFA key obtained in previous auth request, e.g. /api/auth/login
|
||||||
|
device: the device name, used to create an ApiKey associated with this device
|
||||||
|
Output:
|
||||||
|
200 and user info containing:
|
||||||
|
{
|
||||||
|
name: "John Wick",
|
||||||
|
api_key: "a long string"
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify(error="request body cannot be empty"), 400
|
||||||
|
|
||||||
|
mfa_token = data.get("mfa_token")
|
||||||
|
mfa_key = data.get("mfa_key")
|
||||||
|
device = data.get("device")
|
||||||
|
|
||||||
|
s = Signer(FLASK_SECRET)
|
||||||
|
try:
|
||||||
|
user_id = int(s.unsign(mfa_key))
|
||||||
|
except BadSignature:
|
||||||
|
return jsonify(error="Invalid mfa_key"), 400
|
||||||
|
|
||||||
|
user = User.get(user_id)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return jsonify(error="Invalid mfa_key"), 400
|
||||||
|
elif not user.enable_otp:
|
||||||
|
return (
|
||||||
|
jsonify(error="This endpoint should only be used by user who enables MFA"),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
totp = pyotp.TOTP(user.otp_secret)
|
||||||
|
if not totp.verify(mfa_token):
|
||||||
|
return jsonify(error="Wrong TOTP Token"), 400
|
||||||
|
|
||||||
|
ret = {
|
||||||
|
"name": user.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
api_key = ApiKey.create(user.id, device)
|
||||||
|
db.session.commit()
|
||||||
|
ret["api_key"] = api_key.code
|
||||||
|
|
||||||
|
return jsonify(**ret), 200
|
58
tests/api/test_auth_mfa.py
Normal file
58
tests/api/test_auth_mfa.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import pyotp
|
||||||
|
from flask import url_for
|
||||||
|
from itsdangerous import Signer
|
||||||
|
|
||||||
|
from app.config import FLASK_SECRET
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models import User
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_mfa_success(flask_client):
|
||||||
|
user = User.create(
|
||||||
|
email="a@b.c",
|
||||||
|
password="password",
|
||||||
|
name="Test User",
|
||||||
|
activated=True,
|
||||||
|
enable_otp=True,
|
||||||
|
otp_secret="base32secret3232",
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
totp = pyotp.TOTP(user.otp_secret)
|
||||||
|
s = Signer(FLASK_SECRET)
|
||||||
|
mfa_key = s.sign(str(user.id))
|
||||||
|
|
||||||
|
r = flask_client.post(
|
||||||
|
url_for("api.auth_mfa"),
|
||||||
|
json={"mfa_token": totp.now(), "mfa_key": mfa_key, "device": "Test Device"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json["api_key"]
|
||||||
|
assert r.json["name"] == "Test User"
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_wrong_mfa_key(flask_client):
|
||||||
|
user = User.create(
|
||||||
|
email="a@b.c",
|
||||||
|
password="password",
|
||||||
|
name="Test User",
|
||||||
|
activated=True,
|
||||||
|
enable_otp=True,
|
||||||
|
otp_secret="base32secret3232",
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
totp = pyotp.TOTP(user.otp_secret)
|
||||||
|
|
||||||
|
r = flask_client.post(
|
||||||
|
url_for("api.auth_mfa"),
|
||||||
|
json={
|
||||||
|
"mfa_token": totp.now(),
|
||||||
|
"mfa_key": "wrong mfa key",
|
||||||
|
"device": "Test Device",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r.status_code == 400
|
||||||
|
assert r.json["error"]
|
Loading…
Reference in New Issue
Block a user