implement forced password change for generated passwords

This commit is contained in:
Jakob Ketterl 2021-02-08 18:30:54 +01:00
parent ed6594401c
commit 331e9627d6
7 changed files with 93 additions and 23 deletions

32
htdocs/pwchange.html Normal file
View File

@ -0,0 +1,32 @@
<!DOCTYPE HTML>
<html>
<head>
<title>OpenWebRX Password change</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/login.css" />
<script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/Header.js"></script>
<meta charset="utf-8">
</head>
<body>
${header}
<div class="login-container">
<div class="login">
<div class="alert alert-primary">
Your password has been automatically generated and must be changed in order to proceed.
</div>
<form method="POST">
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
</div>
<div class="form-group">
<label for="confirm">Password confirmation</label>
<input type="password" class="form-control" id="confirm" name="confirm" placeholder="Password confirmation">
</div>
<button type="submit" class="btn btn-secondary btn-login">Change password</button>
</form>
</div>
</div>
</body>

View File

@ -10,33 +10,36 @@ logger = logging.getLogger(__name__)
class Authentication(object): class Authentication(object):
def isAuthenticated(self, request): def getUser(self, request):
if "owrx-session" not in request.cookies: if "owrx-session" not in request.cookies:
return False return None
session = SessionStorage.getSharedInstance().getSession(request.cookies["owrx-session"].value) session = SessionStorage.getSharedInstance().getSession(request.cookies["owrx-session"].value)
if session is None: if session is None:
return False return None
if "user" not in session: if "user" not in session:
return False return None
userList = UserList.getSharedInstance() userList = UserList.getSharedInstance()
try: try:
user = userList[session["user"]] return userList[session["user"]]
return user.is_enabled()
except KeyError: except KeyError:
return False return None
class AdminController(WebpageController): class AdminController(WebpageController):
def __init__(self, handler, request, options): def __init__(self, handler, request, options):
self.authentication = Authentication() self.authentication = Authentication()
self.user = self.authentication.getUser(request)
super().__init__(handler, request, options) super().__init__(handler, request, options)
def isAuthorized(self):
return self.user is not None and self.user.is_enabled() and not self.user.must_change_password
def handle_request(self): def handle_request(self):
config = Config.get() config = Config.get()
if "webadmin_enabled" not in config or not config["webadmin_enabled"]: if "webadmin_enabled" not in config or not config["webadmin_enabled"]:
self.send_response("Web Admin is disabled", code=403) self.send_response("Web Admin is disabled", code=403)
return return
if self.authentication.isAuthenticated(self.request): if self.isAuthorized():
super().handle_request() super().handle_request()
else: else:
target = "/login?{0}".format(parse.urlencode({"ref": self.request.path})) target = "/login?{0}".format(parse.urlencode({"ref": self.request.path}))

View File

@ -0,0 +1,23 @@
from owrx.controllers.admin import AdminController
from owrx.users import UserList, DefaultPasswordClass
from urllib.parse import parse_qs
class ProfileController(AdminController):
def isAuthorized(self):
return self.user is not None and self.user.is_enabled() and self.user.must_change_password
def indexAction(self):
self.serve_template("pwchange.html", **self.template_variables())
def processPwChange(self):
data = parse_qs(self.get_body().decode("utf-8"))
data = {k: v[0] for k, v in data.items()}
userlist = UserList.getSharedInstance()
if "password" in data and "confirm" in data and data["password"] == data["confirm"]:
self.user.setPassword(DefaultPasswordClass(data["password"]), must_change_password=False)
userlist.store()
target = self.request.query["ref"][0] if "ref" in self.request.query else "/settings"
else:
target = "/pwchange"
self.send_redirect(target)

View File

@ -1,5 +1,5 @@
from .template import WebpageController from .template import WebpageController
from urllib.parse import parse_qs from urllib.parse import parse_qs, urlencode
from uuid import uuid4 from uuid import uuid4
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
from owrx.users import UserList from owrx.users import UserList
@ -51,6 +51,8 @@ class SessionController(WebpageController):
cookie = SimpleCookie() cookie = SimpleCookie()
cookie["owrx-session"] = key cookie["owrx-session"] = key
target = self.request.query["ref"][0] if "ref" in self.request.query else "/settings" target = self.request.query["ref"][0] if "ref" in self.request.query else "/settings"
if user.must_change_password:
target = "/pwchange?{0}".format(urlencode({"ref": target}))
self.send_redirect(target, cookies=cookie) self.send_redirect(target, cookies=cookie)
return return
self.send_redirect("/login") self.send_redirect("/login")

View File

@ -6,6 +6,7 @@ from owrx.controllers.api import ApiController
from owrx.controllers.metrics import MetricsController from owrx.controllers.metrics import MetricsController
from owrx.controllers.settings import SettingsController, GeneralSettingsController, SdrSettingsController from owrx.controllers.settings import SettingsController, GeneralSettingsController, SdrSettingsController
from owrx.controllers.session import SessionController from owrx.controllers.session import SessionController
from owrx.controllers.profile import ProfileController
from http.server import BaseHTTPRequestHandler from http.server import BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
import re import re
@ -109,6 +110,8 @@ class Router(object):
StaticRoute("/login", SessionController, options={"action": "loginAction"}), StaticRoute("/login", SessionController, options={"action": "loginAction"}),
StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}), StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}),
StaticRoute("/logout", SessionController, options={"action": "logoutAction"}), StaticRoute("/logout", SessionController, options={"action": "logoutAction"}),
StaticRoute("/pwchange", ProfileController),
StaticRoute("/pwchange", ProfileController, method="POST", options={"action": "processPwChange"}),
] ]
def find_route(self, request): def find_route(self, request):

View File

@ -56,7 +56,7 @@ class CleartextPassword(Password):
class HashedPassword(Password): class HashedPassword(Password):
def __init__(self, pwinfo, algorithm="sha256"): def __init__(self, pwinfo, algorithm="sha256"):
self.iterations = 100000 self.iterations = 100000
if (isinstance(pwinfo, str)): if isinstance(pwinfo, str):
self._createFromString(pwinfo, algorithm) self._createFromString(pwinfo, algorithm)
else: else:
self._loadFromDict(pwinfo) self._loadFromDict(pwinfo)
@ -91,20 +91,30 @@ DefaultPasswordClass = HashedPassword
class User(object): class User(object):
def __init__(self, name: str, enabled: bool, password: Password): def __init__(self, name: str, enabled: bool, password: Password, must_change_password: bool = False):
self.name = name self.name = name
self.enabled = enabled self.enabled = enabled
self.password = password self.password = password
self.must_change_password = must_change_password
def toJson(self): def toJson(self):
return { return {
"user": self.name, "user": self.name,
"enabled": self.enabled, "enabled": self.enabled,
"must_change_password": self.must_change_password,
"password": self.password.toJson() "password": self.password.toJson()
} }
def setPassword(self, password: Password): @staticmethod
def fromJson(d):
if "user" in d and "password" in d and "enabled" in d:
mcp = d["must_change_password"] if "must_change_password" in d else False
return User(d["user"], d["enabled"], Password.from_dict(d["password"]), mcp)
def setPassword(self, password: Password, must_change_password: bool = None):
self.password = password self.password = password
if must_change_password is not None:
self.must_change_password = must_change_password
def is_enabled(self): def is_enabled(self):
return self.enabled return self.enabled
@ -150,7 +160,7 @@ class UserList(object):
with open(usersFile, "r") as f: with open(usersFile, "r") as f:
users_json = json.load(f) users_json = json.load(f)
users = {u.name: u for u in [self._jsonToUser(d) for d in users_json]} users = {u.name: u for u in [User.fromJson(d) for d in users_json]}
self.file_modified = modified self.file_modified = modified
return users return users
except FileNotFoundError: except FileNotFoundError:
@ -162,10 +172,6 @@ class UserList(object):
logger.exception("error while processing users from %s", usersFile) logger.exception("error while processing users from %s", usersFile)
return {} return {}
def _jsonToUser(self, d):
if "user" in d and "password" in d and "enabled" in d:
return User(d["user"], d["enabled"], Password.from_dict(d["password"]))
def _userToJson(self, u): def _userToJson(self, u):
return u.toJson() return u.toJson()

View File

@ -27,8 +27,8 @@ class UserCommand(Command, metaclass=ABCMeta):
if args.noninteractive: if args.noninteractive:
print("Generating password for user {username}...".format(username=username)) print("Generating password for user {username}...".format(username=username))
password = self.getRandomPassword() password = self.getRandomPassword()
generated = True
print('Password for {username} is "{password}".'.format(username=username, password=password)) print('Password for {username} is "{password}".'.format(username=username, password=password))
# TODO implement this threat
print('This password is suitable for initial setup only, you will be asked to reset it on initial use.') print('This password is suitable for initial setup only, you will be asked to reset it on initial use.')
print('This password cannot be recovered from the system, please copy it now.') print('This password cannot be recovered from the system, please copy it now.')
else: else:
@ -37,7 +37,8 @@ class UserCommand(Command, metaclass=ABCMeta):
if password != confirm: if password != confirm:
print("ERROR: Password mismatch.") print("ERROR: Password mismatch.")
sys.exit(1) sys.exit(1)
return password generated = False
return password, generated
def getRandomPassword(self, length=10): def getRandomPassword(self, length=10):
printable = list(string.ascii_letters) + list(string.digits) printable = list(string.ascii_letters) + list(string.digits)
@ -52,10 +53,10 @@ class NewUser(UserCommand):
if username in userList: if username in userList:
raise KeyError("User {username} already exists".format(username=username)) raise KeyError("User {username} already exists".format(username=username))
password = self.getPassword(args, username) password, generated = self.getPassword(args, username)
print("Creating user {username}...".format(username=username)) print("Creating user {username}...".format(username=username))
user = User(name=username, enabled=True, password=DefaultPasswordClass(password)) user = User(name=username, enabled=True, password=DefaultPasswordClass(password), must_change_password=generated)
userList.addUser(user) userList.addUser(user)
@ -70,9 +71,9 @@ class DeleteUser(UserCommand):
class ResetPassword(UserCommand): class ResetPassword(UserCommand):
def run(self, args): def run(self, args):
username = self.getUser(args) username = self.getUser(args)
password = self.getPassword(args, username) password, generated = self.getPassword(args, username)
userList = UserList() userList = UserList()
userList[username].setPassword(DefaultPasswordClass(password)) userList[username].setPassword(DefaultPasswordClass(password), must_change_password=generated)
# this is a change to an object in the list, not the list itself # this is a change to an object in the list, not the list itself
# in this case, store() is explicit # in this case, store() is explicit
userList.store() userList.store()