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):
def isAuthenticated(self, request):
def getUser(self, request):
if "owrx-session" not in request.cookies:
return False
return None
session = SessionStorage.getSharedInstance().getSession(request.cookies["owrx-session"].value)
if session is None:
return False
return None
if "user" not in session:
return False
return None
userList = UserList.getSharedInstance()
try:
user = userList[session["user"]]
return user.is_enabled()
return userList[session["user"]]
except KeyError:
return False
return None
class AdminController(WebpageController):
def __init__(self, handler, request, options):
self.authentication = Authentication()
self.user = self.authentication.getUser(request)
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):
config = Config.get()
if "webadmin_enabled" not in config or not config["webadmin_enabled"]:
self.send_response("Web Admin is disabled", code=403)
return
if self.authentication.isAuthenticated(self.request):
if self.isAuthorized():
super().handle_request()
else:
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 urllib.parse import parse_qs
from urllib.parse import parse_qs, urlencode
from uuid import uuid4
from http.cookies import SimpleCookie
from owrx.users import UserList
@ -51,6 +51,8 @@ class SessionController(WebpageController):
cookie = SimpleCookie()
cookie["owrx-session"] = key
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)
return
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.settings import SettingsController, GeneralSettingsController, SdrSettingsController
from owrx.controllers.session import SessionController
from owrx.controllers.profile import ProfileController
from http.server import BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import re
@ -109,6 +110,8 @@ class Router(object):
StaticRoute("/login", SessionController, options={"action": "loginAction"}),
StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}),
StaticRoute("/logout", SessionController, options={"action": "logoutAction"}),
StaticRoute("/pwchange", ProfileController),
StaticRoute("/pwchange", ProfileController, method="POST", options={"action": "processPwChange"}),
]
def find_route(self, request):

View File

@ -56,7 +56,7 @@ class CleartextPassword(Password):
class HashedPassword(Password):
def __init__(self, pwinfo, algorithm="sha256"):
self.iterations = 100000
if (isinstance(pwinfo, str)):
if isinstance(pwinfo, str):
self._createFromString(pwinfo, algorithm)
else:
self._loadFromDict(pwinfo)
@ -91,20 +91,30 @@ DefaultPasswordClass = HashedPassword
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.enabled = enabled
self.password = password
self.must_change_password = must_change_password
def toJson(self):
return {
"user": self.name,
"enabled": self.enabled,
"must_change_password": self.must_change_password,
"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
if must_change_password is not None:
self.must_change_password = must_change_password
def is_enabled(self):
return self.enabled
@ -150,7 +160,7 @@ class UserList(object):
with open(usersFile, "r") as 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
return users
except FileNotFoundError:
@ -162,10 +172,6 @@ class UserList(object):
logger.exception("error while processing users from %s", usersFile)
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):
return u.toJson()

View File

@ -27,8 +27,8 @@ class UserCommand(Command, metaclass=ABCMeta):
if args.noninteractive:
print("Generating password for user {username}...".format(username=username))
password = self.getRandomPassword()
generated = True
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 cannot be recovered from the system, please copy it now.')
else:
@ -37,7 +37,8 @@ class UserCommand(Command, metaclass=ABCMeta):
if password != confirm:
print("ERROR: Password mismatch.")
sys.exit(1)
return password
generated = False
return password, generated
def getRandomPassword(self, length=10):
printable = list(string.ascii_letters) + list(string.digits)
@ -52,10 +53,10 @@ class NewUser(UserCommand):
if username in userList:
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))
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)
@ -70,9 +71,9 @@ class DeleteUser(UserCommand):
class ResetPassword(UserCommand):
def run(self, args):
username = self.getUser(args)
password = self.getPassword(args, username)
password, generated = self.getPassword(args, username)
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
# in this case, store() is explicit
userList.store()