Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
FROM docker.io/python:3.13-slim
MAINTAINER Computer Science House <webmaster@csh.rit.edu>

RUN mkdir /opt/selfservice
WORKDIR /opt/selfservice

COPY requirements.txt /opt/selfservice

WORKDIR /opt/selfservice

RUN apt-get -yq update && \
apt-get -yq install libsasl2-dev libldap2-dev libldap-common libssl-dev git gcc g++ make && \
pip install -r requirements.txt && \
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

1. Run migrations:
1. ```shell script
flask db migrate
flask db upgrade
```

1. Run the application:
Expand Down
20 changes: 17 additions & 3 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
version: '2'
services:
self-service:
build:
context: .
develop:
watch:
- action: sync+restart
path: ./selfservice
target: /opt/selfservice/selfservice
env_file:
- .env
ports:
- "8080:8080"


postgres:
image: docker.io/postgres:9.6
container_name: selfservice-postgres
Expand All @@ -10,7 +24,7 @@ services:
POSTGRES_PASSWORD: supersecretpassword
POSTGRES_USER: selfservice
ports:
- 127.0.0.1:5433:5432
- "127.0.0.1:5433:5432"
phppgadmin:
image: docker.io/dockage/phppgadmin:latest
container_name: selfservice-pgadmin
Expand All @@ -21,5 +35,5 @@ services:
DATABASE_PORT_NUMBER: 5432
restart: always
ports:
- 127.0.0.1:8081:8080
- 127.0.0.1:8444:8443
- "127.0.0.1:8081:8080"
- "127.0.0.1:8444:8443"
42 changes: 42 additions & 0 deletions migrations/versions/92c9d8ea5b74_change_created_to_expires.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""change created to expires

Revision ID: 92c9d8ea5b74
Revises: fdb69cd98e19
Create Date: 2026-03-15 20:16:42.829689

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '92c9d8ea5b74'
down_revision = 'fdb69cd98e19'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('session', schema=None) as batch_op:
batch_op.add_column(sa.Column('expires', sa.DateTime(), nullable=False))
batch_op.drop_column('created')

with op.batch_alter_table('token', schema=None) as batch_op:
batch_op.add_column(sa.Column('expires', sa.DateTime(), nullable=False))
batch_op.drop_column('created')

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('token', schema=None) as batch_op:
batch_op.add_column(sa.Column('created', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
batch_op.drop_column('expires')

with op.batch_alter_table('session', schema=None) as batch_op:
batch_op.add_column(sa.Column('created', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
batch_op.drop_column('expires')

# ### end Alembic commands ###
14 changes: 7 additions & 7 deletions selfservice/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@
ipa = Client(ldap_uri, version="2.215")

# Configure rate limiting
if not app.config["DEBUG"]:
limiter = Limiter(
get_remote_address, app=app, default_limits=["50 per day", "10 per hour"]
)
else:
limiter = Limiter(get_remote_address, app=app, default_limits=[])
# if not app.config["DEBUG"]:
# limiter = Limiter(
# get_remote_address, app=app, default_limits=["50 per day", "10 per hour"]
# )
# else:
# limiter = Limiter(get_remote_address, app=app, default_limits=[])

# Initialize QR Code Generator
qr = QRcode(app)
Expand Down Expand Up @@ -115,7 +115,7 @@ def index():


@app.route("/health")
@limiter.exempt
# @limiter.exempt
def health():
"""
Shows an ok status if the application is up and running
Expand Down
53 changes: 29 additions & 24 deletions selfservice/blueprints/recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
Flask blueprint for handling identity verification and account recovery.
"""

import datetime
import uuid
import logging

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

from selfservice.utilities.general import is_expired, email_recovery, phone_recovery
from selfservice.utilities.general import email_recovery, phone_recovery
from selfservice.utilities.reset import (
generate_token,
generate_pin,
Expand Down Expand Up @@ -90,7 +91,7 @@ def verify_identity(recovery_id):
methods = verif_methods(session.username)

# Make sure it isn't expired.
if is_expired(session.created, 10):
if session.is_expired():
flash("Sorry, your session has expired.")
return redirect("/recovery")

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

# Make sure it isn't expired.
if is_expired(session.created, 10):
if session.is_expired():
flash("Sorry, your session has expired.")
return redirect("/recovery")

Expand Down Expand Up @@ -210,24 +211,23 @@ def reset_password():

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

# Redirect if the token provided isn't valid.
if (
not token
or not token_data
or is_expired(token_data.created, 30)
or token_data.used
):
flash(
"Oops! Invalid or expired reset token. Each token is only "
+ "valid for 30 minutes after it is issued."
)
if not token or not token_data:
flash("Oops! No reset token provided. Please try again.")
return redirect("/recovery")

if token_data.used:
flash("This recovery token has already been used.")
return redirect("/recovery")

if token_data.is_expired():
flash("Oops! Your recovery token expired.")
return redirect("/recovery")

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

# Lets actually do the reset.
# Actually do the reset.
if request.form["password"] == request.form["verify"]:
if len(request.form["password"]) >= 12:
passwd_reset(
Expand Down Expand Up @@ -267,7 +267,12 @@ def admin():
session_id = str(uuid.uuid4())

# Create the object in the database.
session_data = RecoverySession(id=session_id, username=request.form["username"])
session_data = RecoverySession(
id=session_id,
username=request.form["username"],
expires=datetime.datetime.now()
+ datetime.timedelta(hours=int(request.form["expireTime"])),
)
db.session.add(session_data)
db.session.commit()

Expand All @@ -278,28 +283,28 @@ def admin():
last_sessions = [
{
"username": s.username,
"session_created": s.session_created,
"session_expired": (
(is_expired(s.session_created, 10) and not s.token_created)
or is_expired(s.token_created, 30)
s.session_expires < datetime.datetime.now()
or s.token_expires < datetime.datetime.now()
),
"token_created": s.token_created,
"token_exists": s.token_id is not None,
"token_expires": s.token_expires,
"used": s.used,
}
for s in RecoverySession.query.outerjoin(
ResetToken, RecoverySession.id == ResetToken.session
)
.with_entities(
RecoverySession.username,
RecoverySession.created.label("session_created"),
ResetToken.created.label("token_created"),
RecoverySession.expires.label("session_expires"),
ResetToken.id.label("token_id"),
ResetToken.expires.label("token_expires"),
ResetToken.used,
)
.order_by(RecoverySession.created.desc())
.order_by(ResetToken.expires.desc())
.limit(20)
.all()
]

return render_template(
"admin.html",
version=version,
Expand Down
25 changes: 22 additions & 3 deletions selfservice/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
SQLAlchemy Database Models
"""

import datetime
from datetime import timedelta

from sqlalchemy import (
Column,
Integer,
Expand All @@ -24,11 +27,19 @@ class ResetToken(db.Model):
__tablename__ = "token"
id = Column(Integer, primary_key=True)
username = Column(String(64), nullable=False)
created = Column(DateTime, default=func.timezone("UTC", now()))
expires = Column(
DateTime,
default=func.timezone("UTC", now() + timedelta(minutes=10)),
nullable=False,
)
token = Column(String(36))
session = Column(String(36), ForeignKey("session.id"))
used = Column(Boolean)

def is_expired(self) -> bool:
"""Returns whether the Token is expired"""
return self.expires < datetime.datetime.now()


class RecoverySession(db.Model):
"""
Expand All @@ -40,7 +51,15 @@ class RecoverySession(db.Model):
__tablename__ = "session"
id = Column(String(36), primary_key=True)
username = Column(String(64), nullable=False)
created = Column(DateTime, default=func.timezone("UTC", now()))
expires = Column(
DateTime,
default=func.timezone("UTC", now() + timedelta(minutes=30)),
nullable=False,
)

def is_expired(self) -> bool:
"""Returns whether the RecoverySession is expired"""
return self.expires < datetime.datetime.now()


class PhoneVerification(db.Model):
Expand All @@ -56,7 +75,7 @@ class PhoneVerification(db.Model):

class AppSpecificPassword(db.Model):
"""
Allows users to authenticate to applications that don't support two factor
Allows users to authenticate to applications that don't support two-factor
auth. Currently: this is only mail
"""

Expand Down
Loading
Loading