mirror of
https://github.com/janeczku/calibre-web
synced 2026-08-25 04:08:52 +00:00
Improved magic link login flow
This commit is contained in:
+38
-19
@@ -38,6 +38,27 @@ remotelogin = Blueprint('remotelogin', __name__)
|
||||
log = logger.create()
|
||||
|
||||
|
||||
def _load_remote_auth_token(token):
|
||||
auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first()
|
||||
|
||||
# Token not found
|
||||
if auth_token is None:
|
||||
flash(_("Token not found"), category="error")
|
||||
log.error("Remote Login token not found")
|
||||
return None
|
||||
|
||||
# Token expired
|
||||
if datetime.now() > auth_token.expiration:
|
||||
ub.session.delete(auth_token)
|
||||
ub.session_commit()
|
||||
|
||||
flash(_("Token has expired"), category="error")
|
||||
log.error("Remote Login token expired")
|
||||
return None
|
||||
|
||||
return auth_token
|
||||
|
||||
|
||||
def remote_login_required(f):
|
||||
@wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
@@ -56,6 +77,8 @@ def remote_login_required(f):
|
||||
@remote_login_required
|
||||
def remote_login():
|
||||
auth_token = ub.RemoteAuthToken()
|
||||
auth_token.request_ip = request.remote_addr
|
||||
auth_token.request_user_agent = request.headers.get('User-Agent', "")
|
||||
ub.session.add(auth_token)
|
||||
ub.session_commit()
|
||||
verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true)
|
||||
@@ -64,35 +87,31 @@ def remote_login():
|
||||
verify_url=verify_url, page="remotelogin")
|
||||
|
||||
|
||||
@remotelogin.route('/verify/<token>')
|
||||
@remotelogin.route('/verify/<token>', methods=['GET', 'POST'])
|
||||
@remote_login_required
|
||||
@user_login_required
|
||||
def verify_token(token):
|
||||
auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first()
|
||||
|
||||
# Token not found
|
||||
auth_token = _load_remote_auth_token(token)
|
||||
if auth_token is None:
|
||||
flash(_("Token not found"), category="error")
|
||||
log.error("Remote Login token not found")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
# Token expired
|
||||
elif datetime.now() > auth_token.expiration:
|
||||
ub.session.delete(auth_token)
|
||||
if request.method == 'POST':
|
||||
if auth_token.verified:
|
||||
flash(_("Token already approved"), category="error")
|
||||
log.error("Remote Login token already approved")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
auth_token.user_id = current_user.id
|
||||
auth_token.verified = True
|
||||
ub.session_commit()
|
||||
|
||||
flash(_("Token has expired"), category="error")
|
||||
log.error("Remote Login token expired")
|
||||
flash(_("Success! Please return to your device"), category="success")
|
||||
log.debug("Remote Login token for userid %s verified", auth_token.user_id)
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
# Update token with user information
|
||||
auth_token.user_id = current_user.id
|
||||
auth_token.verified = True
|
||||
ub.session_commit()
|
||||
|
||||
flash(_("Success! Please return to your device"), category="success")
|
||||
log.debug("Remote Login token for userid %s verified", auth_token.user_id)
|
||||
return redirect(url_for('web.index'))
|
||||
return render_title_template('remote_login_verify.html', title=_("Login"), token=auth_token.auth_token,
|
||||
request_ip=auth_token.request_ip,
|
||||
request_user_agent=auth_token.request_user_agent, page="remotelogin")
|
||||
|
||||
|
||||
@remotelogin.route('/ajax/verify_token', methods=['POST'])
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
|
||||
(function () {
|
||||
// Poll the server to check if the user has authenticated
|
||||
var csrf_token = $("input[name='csrf_token']").val();
|
||||
var t = setInterval(function () {
|
||||
$.post(getPath() + "/ajax/verify_token", { token: $("#verify_url").data("token") })
|
||||
$.post(getPath() + "/ajax/verify_token", { token: $("#verify_url").data("token"), csrf_token: csrf_token })
|
||||
.done(function(response) {
|
||||
if (response.status === 'success') {
|
||||
// Wait a tick so cookies are updated
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<div class="well col-sm-6 col-sm-offset-2">
|
||||
<h2 style="margin-top: 0">{{ _('Approve Magic Link Login') }}</h2>
|
||||
<p>{{ _('Review the device that requested access before approving this login.') }}</p>
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{{ _('IP address') }}</dt>
|
||||
<dd>{{ request_ip or _('Unknown') }}</dd>
|
||||
<dt>{{ _('Browser') }}</dt>
|
||||
<dd>{{ request_user_agent or _('Unknown') }}</dd>
|
||||
</dl>
|
||||
<p>{{ _('If you approve this request, the device that started it will be logged in as %(username)s.', username=current_user.name) }}</p>
|
||||
<form method="POST" role="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-primary">{{ _('Approve Login') }}</button>
|
||||
<a class="btn btn-default" href="{{ url_for('web.index') }}">{{ _('Cancel') }}</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -534,6 +534,8 @@ class RemoteAuthToken(Base):
|
||||
verified = Column(Boolean, default=False)
|
||||
expiration = Column(DateTime)
|
||||
token_type = Column(Integer, default=0)
|
||||
request_ip = Column(String)
|
||||
request_user_agent = Column(String)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -601,6 +603,18 @@ def migrate_user_session_table(engine, _session):
|
||||
trans.commit()
|
||||
|
||||
|
||||
def migrate_remote_auth_token_table(engine, _session):
|
||||
try:
|
||||
_session.query(exists().where(RemoteAuthToken.request_ip)).scalar()
|
||||
_session.commit()
|
||||
except exc.OperationalError: # Database is not compatible, some columns are missing
|
||||
with engine.connect() as conn:
|
||||
trans = conn.begin()
|
||||
conn.execute(text("ALTER TABLE remote_auth_token ADD column 'request_ip' String"))
|
||||
conn.execute(text("ALTER TABLE remote_auth_token ADD column 'request_user_agent' String"))
|
||||
trans.commit()
|
||||
|
||||
|
||||
# Migrate database to current version, has to be updated after every database change. Currently, migration from
|
||||
# maybe 4/5 versions back to current should work.
|
||||
# Migration is done by checking if relevant columns are existing, and then adding rows with SQL commands
|
||||
@@ -609,6 +623,7 @@ def migrate_Database(_session):
|
||||
add_missing_tables(engine, _session)
|
||||
migrate_registration_table(engine, _session)
|
||||
migrate_user_session_table(engine, _session)
|
||||
migrate_remote_auth_token_table(engine, _session)
|
||||
|
||||
|
||||
def clean_database(_session):
|
||||
|
||||
Reference in New Issue
Block a user