From 78f34be2772a5207612ed58f6326b34b2c941c4f Mon Sep 17 00:00:00 2001 From: girst Date: Thu, 18 Mar 2021 22:49:11 +0100 Subject: [PATCH] [DATABASE CHANGE: Migration below] make login tokens revocable Note that without the migration, nothing except magic-token-login will break. Migration: INSERT INTO user_tokens SELECT id, token FROM users; --- README.md | 1 - app/common/user.py | 47 ++++++++++++++++++++++++------ app/templates/account_mgmt.html.j2 | 11 ++++++- config/setup.sql | 8 ++--- 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b6435ac..4a0ca32 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,6 @@ If you can program in Python or know HTML/CSS, get in touch! - remove deleted videos from subscription feed Note: when a deletion websub comes in, check with `get_video_info` Note: for livestreams, check postlivedvr and queue deletion after it ended - - make magic tokens revocable - cleanups: - split up common/common.py; move stuff into plugins - abstract database access diff --git a/app/common/user.py b/app/common/user.py index ac02a07..194446c 100644 --- a/app/common/user.py +++ b/app/common/user.py @@ -1,6 +1,7 @@ from werkzeug.security import generate_password_hash, check_password_hash from .common import cf import sqlite3 +import secrets from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user from flask import Blueprint, flash, redirect, render_template, url_for, request @@ -11,7 +12,7 @@ class User(UserMixin): # TODO: to common self.passwd = passwd self.token = token def get_id(self): - return self.token + return self.id def set_password(self, passwd): self.passwd = generate_password_hash(passwd) with sqlite3.connect(cf['global']['database']) as conn: @@ -38,14 +39,20 @@ class User(UserMixin): # TODO: to common except: return None # todo: ugly return User(id, name, passwd, token) @classmethod - def from_token(self, token): + def from_token(self, login_token): + # Note: this function reads the revocable token, not the internal one! with sqlite3.connect(cf['global']['database']) as conn: c = conn.cursor() - c.execute("SELECT id,name,password FROM users WHERE token=?", (token,)) + c.execute(""" + SELECT id, name, password, users.token + FROM users JOIN user_tokens ON users.id = user_tokens.user_id + WHERE user_tokens.token = ? + """, (login_token,)) try: - id, name, passwd, = c.fetchone() - except: return None # todo: ugly - return User(id, name, passwd, token) + id, name, passwd, token = c.fetchone() + return User(id, name, passwd, token) + except: + return None def init_login(app): @@ -54,9 +61,9 @@ def init_login(app): login.init_app(app) @login.user_loader - def load_user(token): + def load_user(id): # in the future tokens will be invalidable by users. -> https://flask-login.readthedocs.io/en/latest/#alternative-tokens - return User.from_token(token) + return User.from_id(id) @login.request_loader def querytoken_auth(request): @@ -95,8 +102,21 @@ def init_login(app): return redirect(url_for('usermgmt.login_form')) @usermgmt.route('/manage/account') + @login_required def account_manager(): - return render_template('account_mgmt.html.j2') + with sqlite3.connect(cf['global']['database']) as conn: + c = conn.cursor() + c.execute(""" + SELECT token + FROM user_tokens + WHERE user_id = ? + """, (current_user.id,)) + result = c.fetchone() + if result: + (login_token,) = result + else: + login_token = "" + return render_template('account_mgmt.html.j2', login_token=login_token) @usermgmt.route('/manage/account', methods=['POST']) @login_required @@ -109,6 +129,15 @@ def init_login(app): else: current_user.set_password(request.form.get('newpasswd')) flash('password updated.', 'info') + if action == 'chtok': + with sqlite3.connect(cf['global']['database']) as conn: + new_token = secrets.token_urlsafe(16) + c = conn.cursor() + c.execute(""" + INSERT OR REPLACE INTO user_tokens (user_id, token) + VALUES (?, ?) + """, (current_user.id, new_token)) + flash('new token generated.', 'info') else: flash('unsupported action', 'error') diff --git a/app/templates/account_mgmt.html.j2 b/app/templates/account_mgmt.html.j2 index 7cb1b70..37767d8 100644 --- a/app/templates/account_mgmt.html.j2 +++ b/app/templates/account_mgmt.html.j2 @@ -5,11 +5,20 @@ {% block content %} {{ super() }} -
Change Password
+
Change Password

+
+
+
Login Token +

You can append ?token=... to any URL to + automatically log you in. Generating a new token will + invalidate the old one, but keep logged in sessions alive.

+
+
+
{% endblock %} diff --git a/config/setup.sql b/config/setup.sql index df387e8..b3ef48e 100644 --- a/config/setup.sql +++ b/config/setup.sql @@ -74,7 +74,7 @@ CREATE TABLE users( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, password TEXT NOT NULL, - token TEXT NOT NULL); -- TODO: move into user_tokens table --- CREATE TABLE user_tokens( -- stores retractable url tokens for feeds. --- user_id INTEGER NOT NULL, --- token TEXT NOT NULL); + token TEXT NOT NULL); -- TODO: deprecated; use users.id instead +CREATE TABLE user_tokens( -- stores revocable url tokens for feeds. + user_id INTEGER PRIMARY KEY NOT NULL, + token TEXT NOT NULL); -- 2.39.3