Skip to content

Commit

Permalink
Rebuilt the PwnedHub out-of-band password reset system.
Browse files Browse the repository at this point in the history
  • Loading branch information
lanmaster53 committed May 24, 2024
1 parent 61d737e commit af351fa
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 23 deletions.
29 changes: 29 additions & 0 deletions database/cs/02-pwnedhub.sql
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,35 @@ INSERT INTO `notes` VALUES (1,'2023-05-31 14:39:06','2023-05-31 15:09:26','Notes
/*!40000 ALTER TABLE `notes` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `tokens`
--

DROP TABLE IF EXISTS `tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `tokens` (
`id` int NOT NULL AUTO_INCREMENT,
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
`value` varchar(255) NOT NULL,
`ttl` int NOT NULL,
`user_id` int NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `tokens_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `tokens`
--

LOCK TABLES `tokens` WRITE;
/*!40000 ALTER TABLE `tokens` DISABLE KEYS */;
/*!40000 ALTER TABLE `tokens` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `tools`
--
Expand Down
29 changes: 29 additions & 0 deletions database/ctf/02-pwnedhub.sql
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,35 @@ LOCK TABLES `notes` WRITE;
/*!40000 ALTER TABLE `notes` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `tokens`
--

DROP TABLE IF EXISTS `tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `tokens` (
`id` int NOT NULL AUTO_INCREMENT,
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
`value` varchar(255) NOT NULL,
`ttl` int NOT NULL,
`user_id` int NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `tokens_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `tokens`
--

LOCK TABLES `tokens` WRITE;
/*!40000 ALTER TABLE `tokens` DISABLE KEYS */;
/*!40000 ALTER TABLE `tokens` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `tools`
--
Expand Down
29 changes: 29 additions & 0 deletions database/init/02-pwnedhub.sql
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,35 @@ LOCK TABLES `notes` WRITE;
/*!40000 ALTER TABLE `notes` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `tokens`
--

DROP TABLE IF EXISTS `tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `tokens` (
`id` int NOT NULL AUTO_INCREMENT,
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
`value` varchar(255) NOT NULL,
`ttl` int NOT NULL,
`user_id` int NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `tokens_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `tokens`
--

LOCK TABLES `tokens` WRITE;
/*!40000 ALTER TABLE `tokens` DISABLE KEYS */;
/*!40000 ALTER TABLE `tokens` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `tools`
--
Expand Down
31 changes: 31 additions & 0 deletions pwnedhub/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ class User(BaseModel):
status = db.Column(db.Integer, nullable=False, default=1)
notes = db.relationship('Note', back_populates='owner', lazy='dynamic')
messages = db.relationship('Message', back_populates='author', lazy='dynamic')
tokens = db.relationship('Token', back_populates='owner', lazy='dynamic')
sent_mail = db.relationship('Mail', foreign_keys=[Mail.sender_id], back_populates='sender', lazy='dynamic')
received_mail = db.relationship('Mail', foreign_keys=[Mail.receiver_id], back_populates='receiver', lazy='dynamic')

Expand Down Expand Up @@ -187,3 +188,33 @@ def get_by_email(email):

def __repr__(self):
return "<User '{}'>".format(self.username)


class Token(BaseModel):
__tablename__ = 'tokens'
value = db.Column(db.String(255), nullable=False)
ttl = db.Column(db.Integer, nullable=False, default=600)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
owner = db.relationship('User', back_populates='tokens')

@property
def is_valid(self):
current = get_current_utc_time().replace(tzinfo=None)
diff = current - self.created
if diff.total_seconds() > self.ttl:
return False
return True

@staticmethod
def get_by_value(value):
return Token.query.filter_by(value=value).first()

@staticmethod
def purge():
invalid_tokens = [t for t in Token.query.all() if not t.is_valid]
for invalid_token in invalid_tokens:
db.session.delete(invalid_token)
db.session.commit()

def __repr__(self):
return "<Token '{}'>".format(self.value)
1 change: 1 addition & 0 deletions pwnedhub/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ <h4>Login with your<br/>Google Account</h4>
{% else %}
<form class="flex-column form" action="{{ url_for('auth.login', next=next) }}" method="post">
{% endif %}
<h3 style="text-align: center;">Please log in.</h3>
<label for="username">Username:</label>
<input name="username" type="text" />
<label for="password">Password:</label>
Expand Down
1 change: 1 addition & 0 deletions pwnedhub/templates/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{% block body %}
<div class="flex-width-4 flex-offset-4 flex-basis-4">
<form class="flex-column" action="{{ url_for('auth.register') }}" method="post">
<h3 style="text-align: center;">Create an account.</h3>
<label for="username">Username: *</label>
<input name="username" type="text" />
<label for="email">Email: *</label>
Expand Down
1 change: 1 addition & 0 deletions pwnedhub/templates/reset_init.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{% block body %}
<div class="flex-width-4 flex-offset-4 flex-basis-4">
<form class="flex-column" action="{{ url_for('auth.reset_init') }}" method="post">
<h3 style="text-align: center;">Password trouble?</h3>
<label for="username">Username:</label>
<input name="username" type="text" value="" />
<input type="submit" value="Submit" onclick="cleanSubmit(event, this.form);" />
Expand Down
7 changes: 6 additions & 1 deletion pwnedhub/templates/reset_password.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
{% extends "layout.html" %}
{% block body %}
<div class="flex-width-4 flex-offset-4 flex-basis-4">
{% if app_config('OOB_RESET_ENABLE') %}
<form class="flex-column" action="{{ url_for('auth.reset_password_oob', token=token) }}" method="post">
{% else %}
<form class="flex-column" action="{{ url_for('auth.reset_password') }}" method="post">
<label for="password">New Password ({{ user.name }}):</label>
{% endif %}
<h3 style="text-align: center;">Welcome back!</h3>
<label for="password">New Password for {{ user.name }}:</label>
<div class="flex-column" style="position: relative;">
<input id="password" name="password" type="password" />
<input type="button" class="show" tabindex="-1" onclick="toggleShow();" value="show" />
Expand Down
1 change: 1 addition & 0 deletions pwnedhub/templates/reset_question.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{% block body %}
<div class="flex-width-4 flex-offset-4 flex-basis-4">
<form class="flex-column" action="{{ url_for('auth.reset_question') }}" method="post">
<h3 style="text-align: center;">Verify your identity.</h3>
<label for="answer">{{ question }}</label>
<input name="answer" type="text" value="" />
<input type="submit" value="Submit" onclick="cleanSubmit(event, this.form);" />
Expand Down
62 changes: 40 additions & 22 deletions pwnedhub/views/auth.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from flask import Blueprint, current_app, request, g, session, redirect, url_for, render_template, flash
from flask import Blueprint, current_app, request, g, session, redirect, url_for, render_template, flash, abort
from pwnedhub import db
from pwnedhub.constants import QUESTIONS
from pwnedhub.decorators import validate
from pwnedhub.models import Config, Email, Mail, User
from pwnedhub.models import Config, Email, Mail, User, Token
from pwnedhub.oauth import OAuthSignIn, OAuthCallbackError
from pwnedhub.utils import xor_encrypt, generate_timestamp_token
from pwnedhub.validators import is_valid_password
Expand Down Expand Up @@ -178,12 +178,15 @@ def reset_init():
except:
user = None
if user:
session['reset_id'] = user.id
if Config.get_value('OOB_RESET_ENABLE'):
# begin the out-of-band reset flow
reset_token = generate_timestamp_token()
session['reset_token'] = reset_token
link = url_for('auth.reset_verify', code=reset_token, _external=True)
# initialize the out-of-band reset flow
reset_token = Token(
value=generate_timestamp_token(),
owner=user
)
db.session.add(reset_token)
db.session.commit()
link = url_for('auth.reset_password_oob', token=reset_token.value, _external=True)
email = Email(
sender = '[email protected]',
receiver = user.email,
Expand All @@ -194,7 +197,8 @@ def reset_init():
db.session.commit()
flash('Check your email to reset your password.')
return redirect(url_for('auth.reset_init'))
# begin the in-band reset flow
# initialize the in-band reset flow
session['reset_id'] = user.id
return redirect(url_for('auth.reset_question'))
else:
flash('User not recognized.')
Expand All @@ -203,6 +207,8 @@ def reset_init():
@blp.route('/reset/question', methods=['GET', 'POST'])
@validate(['answer'])
def reset_question():
if Config.get_value('OOB_RESET_ENABLE'):
abort(404)
# validate flow control
if not session.get('reset_id'):
return reset_flow('Reset improperly initialized.')
Expand All @@ -214,20 +220,11 @@ def reset_question():
return reset_flow('Incorrect answer.')
return render_template('reset_question.html', question=user.question_as_string)

@blp.route('/reset/verify')
@validate(['code'], method='GET')
def reset_verify():
# validate flow control
if not session.get('reset_id'):
return reset_flow('Reset improperly initialized.')
code = request.args.get('code')
if code == session.pop('reset_token', None):
return redirect(url_for('auth.reset_password'))
return reset_flow('Invalid reset token.')

@blp.route('/reset/password', methods=['GET', 'POST'])
@validate(['password'])
def reset_password():
if Config.get_value('OOB_RESET_ENABLE'):
abort(404)
# validate flow control
if not session.get('reset_id'):
return reset_flow('Reset improperly initialized.')
Expand All @@ -237,10 +234,31 @@ def reset_password():
if is_valid_password(password):
session.pop('reset_id', None)
user.password = password
db.session.add(user)
db.session.commit()
flash('Password reset. Please log in.')
return redirect(url_for('auth.login'))
else:
flash('Password does not meet complexity requirements.')
flash('Password does not meet complexity requirements.')
return render_template('reset_password.html', user=user)

@blp.route('/reset/password/<string:token>', methods=['GET', 'POST'])
@validate(['password'])
def reset_password_oob(token):
if not Config.get_value('OOB_RESET_ENABLE'):
abort(404)
# validate the reset token
reset_token = Token.get_by_value(token)
if not reset_token:
return reset_flow('Invalid reset token.')
if not reset_token.is_valid:
Token.purge()
return reset_flow('Invalid reset token.')
if request.method == 'POST':
password = request.form['password']
if is_valid_password(password):
reset_token.owner.password = password
db.session.delete(reset_token)
db.session.commit()
flash('Password reset. Please log in.')
return redirect(url_for('auth.login'))
flash('Password does not meet complexity requirements.')
return render_template('reset_password.html', token=reset_token.value, user=reset_token.owner)

0 comments on commit af351fa

Please sign in to comment.