Add "magic link" functionality

When using a device that is bothersome to log in on (e.g. a Kindle) you can use a magic link to log in via another device.

Configuration was added and is disabled by default.
This commit is contained in:
Jonathan Rehm
2017-07-07 18:29:16 -07:00
parent 5276bda153
commit 623f5c8ef0
8 changed files with 221 additions and 2 deletions
+109 -2
View File
@@ -58,6 +58,7 @@ import shutil
import gdriveutils
import tempfile
import hashlib
from redirect import redirect_back, is_safe_url
from tornado import version as tornadoVersion
@@ -372,6 +373,21 @@ def login_required_if_no_ano(func):
return login_required(func)
def remote_login_required(f):
@wraps(f)
def inner(*args, **kwargs):
if config.config_remote_login:
return f(*args, **kwargs)
if request.is_xhr:
data = {'status': 'error', 'message': 'Forbidden'}
response = make_response(json.dumps(data, ensure_ascii=false))
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response, 403
abort(403)
return inner
# custom jinja filters
@app.template_filter('shortentitle')
def shortentitle_filter(s):
@@ -1805,14 +1821,20 @@ def login():
if user and check_password_hash(user.password, form['password']):
login_user(user, remember=True)
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success")
return redirect(url_for("index"))
return redirect_back(url_for("index"))
else:
ipAdress=request.headers.get('X-Forwarded-For', request.remote_addr)
app.logger.info('Login failed for user "' + form['username'] + '" IP-adress: ' + ipAdress)
flash(_(u"Wrong Username or Password"), category="error")
return render_title_template('login.html', title=_(u"login"))
next_url = request.args.get('next')
if next_url is None or not is_safe_url(next_url):
next_url = url_for('index')
return render_title_template('login.html', title=_(u"login"), next_url=next_url,
remote_login=config.config_remote_login)
@app.route('/logout')
@@ -1823,6 +1845,87 @@ def logout():
return redirect(url_for('login'))
@app.route('/remote/login')
@remote_login_required
def remote_login():
auth_token = ub.RemoteAuthToken()
ub.session.add(auth_token)
ub.session.commit()
verify_url = url_for('verify_token', token=auth_token.auth_token, _external=true)
return render_title_template('remote_login.html', title=_(u"login"), token=auth_token.auth_token,
verify_url=verify_url)
@app.route('/verify/<token>')
@remote_login_required
@login_required
def verify_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(_(u"Token not found"), category="error")
return redirect(url_for('index'))
# Token expired
if datetime.datetime.now() > auth_token.expiration:
ub.session.delete(auth_token)
ub.session.commit()
flash(_(u"Token has expired"), category="error")
return redirect(url_for('index'))
# Update token with user information
auth_token.user_id = current_user.id
auth_token.verified = True
ub.session.commit()
flash(_(u"Success! Please return to your device"), category="success")
return redirect(url_for('index'))
@app.route('/ajax/verify_token', methods=['POST'])
@remote_login_required
def token_verified():
token = request.form['token']
auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first()
data = {}
# Token not found
if auth_token is None:
data['status'] = 'error'
data['message'] = _(u"Token not found")
# Token expired
elif datetime.datetime.now() > auth_token.expiration:
ub.session.delete(auth_token)
ub.session.commit()
data['status'] = 'error'
data['message'] = _(u"Token has expired")
elif not auth_token.verified:
data['status'] = 'not_verified'
else:
user = ub.session.query(ub.User).filter(ub.User.id == auth_token.user_id).first()
login_user(user)
ub.session.delete(auth_token)
ub.session.commit()
data['status'] = 'success'
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success")
response = make_response(json.dumps(data, ensure_ascii=false))
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@app.route('/send/<int:book_id>')
@login_required
@download_required
@@ -2178,6 +2281,10 @@ def configuration_helper(origin):
content.config_anonbrowse = 1
if "config_public_reg" in to_save and to_save["config_public_reg"] == "on":
content.config_public_reg = 1
content.config_remote_login = ("config_remote_login" in to_save and to_save["config_remote_login"] == "on")
if not content.config_remote_login:
ub.session.query(ub.RemoteAuthToken).delete()
content.config_default_role = 0
if "admin_role" in to_save: