Adding argon2 hashing to user model
continuous-integration/drone/push Build is failing Details

master
Drew Bednar 1 year ago
parent af854f448b
commit 910ad0aedf

@ -26,6 +26,7 @@ For a list of current utilities.
- [x] User accounts - [x] User accounts
- [ ] Flask Login integration - [ ] Flask Login integration
- [ ] Oauth Integration - [ ] Oauth Integration
- [ ] Secure password hashing with [argon2](https://www.password-hashing.net/)
- [ ] RBAC for contact sharing - [ ] RBAC for contact sharing
- [ ] Email integration - [ ] Email integration
- [ ] Admin portal - [ ] Admin portal

@ -1,15 +1,26 @@
from flask import Flask from flask import Flask
from flask_login import LoginManager
from .config import ContactSettings from .config import ContactSettings
# Configure Authentication
login_manager = LoginManager()
login_manager.session_protection = "strong"
login_manager.login_view = "user.user_login"
def create_app(config: ContactSettings = None): def create_app(config: ContactSettings = None):
app = Flask("htmx_contact") app = Flask("htmx_contact")
app.config.from_object(config if config else ContactSettings()) app.config.from_object(config if config else ContactSettings())
login_manager.init_app(app)
from . import main from . import main
app.register_blueprint(main.bp) app.register_blueprint(main.bp)
from . import user
app.register_blueprint(user.bp)
return app return app

@ -1,6 +1,8 @@
import re import re
from typing import List from typing import List
from argon2 import PasswordHasher
from flask_login import UserMixin
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from sqlalchemy import String from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
@ -16,20 +18,36 @@ def _is_valid_phone_number(phone_number):
return bool(re.match(PHONE_REGEX, phone_number)) return bool(re.match(PHONE_REGEX, phone_number))
# TODO make hashing values configurable
ph = PasswordHasher()
class Base(DeclarativeBase): class Base(DeclarativeBase):
pass pass
class User(Base): class User(Base, UserMixin):
__tablename__ = "user" __tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(30), unique=True) username: Mapped[str] = mapped_column(String(30), unique=True)
primary_email: Mapped[str] = mapped_column(String(120), unique=True) primary_email: Mapped[str] = mapped_column(String(120), unique=True)
password_hash: Mapped[str] = mapped_column(String())
contacts: Mapped[List["Contact"]] = relationship(back_populates="user", cascade="all, delete-orphan") contacts: Mapped[List["Contact"]] = relationship(back_populates="user", cascade="all, delete-orphan")
def __repr__(self) -> str: def __repr__(self) -> str:
return f"User(id={self.id!r}, username={self.username!r})" # see format specifiers for !r return f"User(id={self.id!r}, username={self.username!r})" # see format specifiers for !r
@property
def password(self):
raise AttributeError('password: write-only field')
@password.setter
def password(self, password):
self.password_hash = ph.hash(password)
def check_password(self, password):
return ph.verify(self.password_hash, password)
class Contact(Base): class Contact(Base):
__tablename__ = "contact" __tablename__ = "contact"

@ -0,0 +1,18 @@
from flask import Blueprint
bp = Blueprint("user", __name__, url_prefix="/user")
@bp.route("/login", methods=["GET", "POST"])
def user_login():
pass
@bp.route("/logout", methods=["GET"])
def user_logout():
pass
@bp.route("/sign-up")
def user_sign_up():
pass

@ -0,0 +1,30 @@
"""empty message
Revision ID: f06a2e34836b
Revises: d76ecaeb13db
Create Date: 2023-09-23 07:58:10.338559
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'f06a2e34836b'
down_revision: Union[str, None] = 'd76ecaeb13db'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('password_hash', sa.String(), nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'password_hash')
# ### end Alembic commands ###

@ -1,4 +1,5 @@
alembic>=1.12.0,<2.0 alembic>=1.12.0,<2.0
argon2-cffi==23.1.0
flask>=2.3.3,<3.0 flask>=2.3.3,<3.0
flask-login>=0.6.2,<1.0 flask-login>=0.6.2,<1.0
pydantic>=2.3.0,<3.0 pydantic>=2.3.0,<3.0

@ -8,8 +8,14 @@ alembic==1.12.0
# via -r requirements.in # via -r requirements.in
annotated-types==0.5.0 annotated-types==0.5.0
# via pydantic # via pydantic
argon2-cffi==23.1.0
# via -r requirements.in
argon2-cffi-bindings==21.2.0
# via argon2-cffi
blinker==1.6.2 blinker==1.6.2
# via flask # via flask
cffi==1.15.1
# via argon2-cffi-bindings
click==8.1.7 click==8.1.7
# via flask # via flask
flask==2.3.3 flask==2.3.3
@ -31,6 +37,8 @@ markupsafe==2.1.3
# jinja2 # jinja2
# mako # mako
# werkzeug # werkzeug
pycparser==2.21
# via cffi
pydantic==2.3.0 pydantic==2.3.0
# via # via
# -r requirements.in # -r requirements.in

Loading…
Cancel
Save