Skip to content

Commit 9600c05

Browse files
expires instead of created. custom expiry times (#120)
* expires instead of created. custom expiry times * lint fix * lint fix 2 * changed migrations to rename column to not create null values * fix sessions display * lint --------- Co-authored-by: Noah Hanford (spaced) <spaced@csh.rit.edu>
1 parent 0db9c46 commit 9600c05

11 files changed

Lines changed: 282 additions & 202 deletions

File tree

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.docker

Dockerfile

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
FROM docker.io/python:3.13-slim
22
MAINTAINER Computer Science House <webmaster@csh.rit.edu>
33

4-
RUN mkdir /opt/selfservice
4+
WORKDIR /opt/selfservice
55

66
COPY requirements.txt /opt/selfservice
77

8-
WORKDIR /opt/selfservice
9-
108
RUN apt-get -yq update && \
119
apt-get -yq install libsasl2-dev libldap2-dev libldap-common libssl-dev git gcc g++ make && \
1210
pip install -r requirements.txt && \

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
1. Run migrations:
3535
1. ```shell script
36-
flask db migrate
36+
flask db upgrade
3737
```
3838

3939
1. Run the application:

docker-compose.yaml

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
11
version: '2'
22
services:
3+
self-service:
4+
build:
5+
context: .
6+
develop:
7+
watch:
8+
- action: sync+restart
9+
path: ./selfservice
10+
target: /opt/selfservice/selfservice
11+
env_file:
12+
- .env
13+
ports:
14+
- "8080:8080"
15+
16+
317
postgres:
4-
image: docker.io/postgres:9.6
18+
image: docker.io/postgres:17
519
container_name: selfservice-postgres
620
restart: always
721
volumes:
@@ -10,16 +24,14 @@ services:
1024
POSTGRES_PASSWORD: supersecretpassword
1125
POSTGRES_USER: selfservice
1226
ports:
13-
- 127.0.0.1:5433:5432
27+
- "127.0.0.1:5433:5432"
1428
phppgadmin:
1529
image: docker.io/dockage/phppgadmin:latest
1630
container_name: selfservice-pgadmin
17-
links:
18-
- postgres
1931
environment:
2032
DATABASE_HOST: postgres
2133
DATABASE_PORT_NUMBER: 5432
2234
restart: always
2335
ports:
24-
- 127.0.0.1:8081:8080
25-
- 127.0.0.1:8444:8443
36+
- "127.0.0.1:8081:8080"
37+
- "127.0.0.1:8444:8443"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""change created to expires
2+
3+
Revision ID: 92c9d8ea5b74
4+
Revises: fdb69cd98e19
5+
Create Date: 2026-03-15 20:16:42.829689
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlalchemy.dialects import postgresql
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '92c9d8ea5b74'
14+
down_revision = 'fdb69cd98e19'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
with op.batch_alter_table('session', schema=None) as batch_op:
22+
batch_op.alter_column('created', new_column_name='expires')
23+
24+
with op.batch_alter_table('token', schema=None) as batch_op:
25+
batch_op.alter_column('created', new_column_name='expires')
26+
27+
# ### end Alembic commands ###
28+
29+
30+
def downgrade():
31+
# ### commands auto generated by Alembic - please adjust! ###
32+
with op.batch_alter_table('token', schema=None) as batch_op:
33+
batch_op.alter_column('expires', new_column_name='created')
34+
35+
with op.batch_alter_table('session', schema=None) as batch_op:
36+
batch_op.alter_column('expires', new_column_name='created')
37+
38+
# ### end Alembic commands ###

selfservice/__init__.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@
7272
ipa = Client(ldap_uri, version="2.215")
7373

7474
# Configure rate limiting
75-
if not app.config["DEBUG"]:
76-
limiter = Limiter(
77-
get_remote_address, app=app, default_limits=["50 per day", "10 per hour"]
78-
)
79-
else:
80-
limiter = Limiter(get_remote_address, app=app, default_limits=[])
75+
# if not app.config["DEBUG"]:
76+
# limiter = Limiter(
77+
# get_remote_address, app=app, default_limits=["50 per day", "10 per hour"]
78+
# )
79+
# else:
80+
# limiter = Limiter(get_remote_address, app=app, default_limits=[])
8181

8282
# Initialize QR Code Generator
8383
qr = QRcode(app)
@@ -115,7 +115,7 @@ def index():
115115

116116

117117
@app.route("/health")
118-
@limiter.exempt
118+
# @limiter.exempt
119119
def health():
120120
"""
121121
Shows an ok status if the application is up and running

selfservice/blueprints/recovery.py

Lines changed: 41 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
Flask blueprint for handling identity verification and account recovery.
33
"""
44

5+
import datetime
56
import uuid
67
import logging
78

89
from flask import Blueprint, render_template, request, redirect, flash
910
from flask import session as flask_session
1011

11-
from selfservice.utilities.general import is_expired, email_recovery, phone_recovery
12+
from selfservice.utilities.general import email_recovery, phone_recovery
1213
from selfservice.utilities.reset import (
1314
generate_token,
1415
generate_pin,
@@ -90,7 +91,7 @@ def verify_identity(recovery_id):
9091
methods = verif_methods(session.username)
9192

9293
# Make sure it isn't expired.
93-
if is_expired(session.created, 10):
94+
if session.is_expired():
9495
flash("Sorry, your session has expired.")
9596
return redirect("/recovery")
9697

@@ -132,7 +133,7 @@ def method_selection(recovery_id, method):
132133
methods = verif_methods(session.username)
133134

134135
# Make sure it isn't expired.
135-
if is_expired(session.created, 10):
136+
if session.is_expired():
136137
flash("Sorry, your session has expired.")
137138
return redirect("/recovery")
138139

@@ -210,24 +211,23 @@ def reset_password():
210211

211212
token_data = ResetToken.query.filter_by(token=token).first()
212213

213-
# Redirect if the token provided isn't valid.
214-
if (
215-
not token
216-
or not token_data
217-
or is_expired(token_data.created, 30)
218-
or token_data.used
219-
):
220-
flash(
221-
"Oops! Invalid or expired reset token. Each token is only "
222-
+ "valid for 30 minutes after it is issued."
223-
)
214+
if not token or not token_data:
215+
flash("Oops! No reset token provided. Please try again.")
216+
return redirect("/recovery")
217+
218+
if token_data.used:
219+
flash("This recovery token has already been used.")
220+
return redirect("/recovery")
221+
222+
if token_data.is_expired():
223+
flash("Oops! Your recovery token expired.")
224224
return redirect("/recovery")
225225

226226
# Display the reset page.
227227
if request.method == "GET":
228228
return render_template("reset.html", token=token_data.token, version=version)
229229

230-
# Lets actually do the reset.
230+
# Actually do the reset.
231231
if request.form["password"] == request.form["verify"]:
232232
if len(request.form["password"]) >= 12:
233233
passwd_reset(
@@ -267,39 +267,47 @@ def admin():
267267
session_id = str(uuid.uuid4())
268268

269269
# Create the object in the database.
270-
session_data = RecoverySession(id=session_id, username=request.form["username"])
270+
session_data = RecoverySession(
271+
id=session_id,
272+
username=request.form["username"],
273+
expires=datetime.datetime.now()
274+
+ datetime.timedelta(hours=int(request.form["expireTime"])),
275+
)
271276
db.session.add(session_data)
272277
db.session.commit()
273278

274279
token = generate_token(session_data)
275280

276281
members = get_members()
277282
uid = str(flask_session["userinfo"].get("preferred_username", ""))
283+
284+
last_sessions_query = (
285+
RecoverySession.query.join(ResetToken, RecoverySession.id == ResetToken.session)
286+
.with_entities(
287+
RecoverySession.username,
288+
RecoverySession.expires.label("session_expires"),
289+
ResetToken.id.label("token_id"),
290+
ResetToken.expires.label("token_expires"),
291+
ResetToken.used,
292+
)
293+
.order_by(ResetToken.expires.desc())
294+
.limit(20)
295+
.all()
296+
)
297+
278298
last_sessions = [
279299
{
280300
"username": s.username,
281-
"session_created": s.session_created,
282301
"session_expired": (
283-
(is_expired(s.session_created, 10) and not s.token_created)
284-
or is_expired(s.token_created, 30)
302+
s.session_expires < datetime.datetime.now()
303+
or s.token_expires < datetime.datetime.now()
285304
),
286-
"token_created": s.token_created,
305+
"token_exists": s.token_id is not None,
306+
"token_expires": s.token_expires,
287307
"used": s.used,
288308
}
289-
for s in RecoverySession.query.outerjoin(
290-
ResetToken, RecoverySession.id == ResetToken.session
291-
)
292-
.with_entities(
293-
RecoverySession.username,
294-
RecoverySession.created.label("session_created"),
295-
ResetToken.created.label("token_created"),
296-
ResetToken.used,
297-
)
298-
.order_by(RecoverySession.created.desc())
299-
.limit(20)
300-
.all()
309+
for s in last_sessions_query
301310
]
302-
303311
return render_template(
304312
"admin.html",
305313
version=version,

selfservice/models.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
SQLAlchemy Database Models
33
"""
44

5+
import datetime
6+
from datetime import timedelta
7+
58
from sqlalchemy import (
69
Column,
710
Integer,
@@ -24,11 +27,19 @@ class ResetToken(db.Model):
2427
__tablename__ = "token"
2528
id = Column(Integer, primary_key=True)
2629
username = Column(String(64), nullable=False)
27-
created = Column(DateTime, default=func.timezone("UTC", now()))
30+
expires = Column(
31+
DateTime,
32+
default=func.timezone("UTC", now() + timedelta(minutes=10)),
33+
nullable=False,
34+
)
2835
token = Column(String(36))
2936
session = Column(String(36), ForeignKey("session.id"))
3037
used = Column(Boolean)
3138

39+
def is_expired(self) -> bool:
40+
"""Returns whether the Token is expired"""
41+
return self.expires < datetime.datetime.now()
42+
3243

3344
class RecoverySession(db.Model):
3445
"""
@@ -40,7 +51,15 @@ class RecoverySession(db.Model):
4051
__tablename__ = "session"
4152
id = Column(String(36), primary_key=True)
4253
username = Column(String(64), nullable=False)
43-
created = Column(DateTime, default=func.timezone("UTC", now()))
54+
expires = Column(
55+
DateTime,
56+
default=func.timezone("UTC", now() + timedelta(minutes=30)),
57+
nullable=False,
58+
)
59+
60+
def is_expired(self) -> bool:
61+
"""Returns whether the RecoverySession is expired"""
62+
return self.expires < datetime.datetime.now()
4463

4564

4665
class PhoneVerification(db.Model):
@@ -56,7 +75,7 @@ class PhoneVerification(db.Model):
5675

5776
class AppSpecificPassword(db.Model):
5877
"""
59-
Allows users to authenticate to applications that don't support two factor
78+
Allows users to authenticate to applications that don't support two-factor
6079
auth. Currently: this is only mail
6180
"""
6281

0 commit comments

Comments
 (0)