Store transfer tokens hashed in the db and only allow them to be valid for 24 hours (#1080)
* Store transfer tokens hashed in the db and only allow them to be valid for 30 mins Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
This commit is contained in:
parent
91b3e05ed6
commit
efa534fd3e
|
@ -453,6 +453,9 @@ if len(VERP_EMAIL_SECRET) < 32:
|
|||
raise RuntimeError(
|
||||
"Please, set VERP_EMAIL_SECRET to a random string at least 32 chars long"
|
||||
)
|
||||
ALIAS_TRANSFER_TOKEN_SECRET = os.environ.get("ALIAS_TRANSFER_TOKEN_SECRET") or (
|
||||
FLASK_SECRET + "aliastransfertoken"
|
||||
)
|
||||
|
||||
|
||||
def get_allowed_redirect_domains() -> List[str]:
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
from uuid import uuid4
|
||||
import base64
|
||||
import hmac
|
||||
import secrets
|
||||
|
||||
import arrow
|
||||
from flask import render_template, redirect, url_for, flash, request
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from app.config import URL
|
||||
from app import config
|
||||
from app.dashboard.base import dashboard_bp
|
||||
from app.dashboard.views.enter_sudo import sudo_required
|
||||
from app.db import Session
|
||||
|
@ -76,6 +79,15 @@ def transfer(alias, new_user, new_mailboxes: [Mailbox]):
|
|||
Session.commit()
|
||||
|
||||
|
||||
def hmac_alias_transfer_token(transfer_token: str) -> str:
|
||||
alias_hmac = hmac.new(
|
||||
config.ALIAS_TRANSFER_TOKEN_SECRET.encode("utf-8"),
|
||||
transfer_token.encode("utf-8"),
|
||||
"sha3_224",
|
||||
)
|
||||
return base64.urlsafe_b64encode(alias_hmac.digest()).decode("utf-8").rstrip("=")
|
||||
|
||||
|
||||
@dashboard_bp.route("/alias_transfer/send/<int:alias_id>/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@sudo_required
|
||||
|
@ -92,37 +104,35 @@ def alias_transfer_send_route(alias_id):
|
|||
)
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
if alias.transfer_token:
|
||||
alias_transfer_url = (
|
||||
URL + "/dashboard/alias_transfer/receive" + f"?token={alias.transfer_token}"
|
||||
)
|
||||
else:
|
||||
alias_transfer_url = None
|
||||
alias_transfer_url = None
|
||||
|
||||
# generate a new transfer_token
|
||||
if request.method == "POST":
|
||||
# generate a new transfer_token
|
||||
if request.form.get("form-name") == "create":
|
||||
alias.transfer_token = str(uuid4())
|
||||
transfer_token = f"{alias.id}.{secrets.token_urlsafe(32)}"
|
||||
alias.transfer_token = hmac_alias_transfer_token(transfer_token)
|
||||
alias.transfer_token_expiration = arrow.utcnow().shift(hours=24)
|
||||
Session.commit()
|
||||
alias_transfer_url = (
|
||||
URL
|
||||
config.URL
|
||||
+ "/dashboard/alias_transfer/receive"
|
||||
+ f"?token={alias.transfer_token}"
|
||||
+ f"?token={transfer_token}"
|
||||
)
|
||||
flash("Share URL created", "success")
|
||||
return redirect(request.url)
|
||||
flash("Share alias URL created", "success")
|
||||
# request.form.get("form-name") == "remove"
|
||||
else:
|
||||
alias.transfer_token = None
|
||||
alias.transfer_token_expiration = None
|
||||
Session.commit()
|
||||
alias_transfer_url = None
|
||||
flash("Share URL deleted", "success")
|
||||
return redirect(request.url)
|
||||
|
||||
return render_template(
|
||||
"dashboard/alias_transfer_send.html",
|
||||
alias=alias,
|
||||
alias_transfer_url=alias_transfer_url,
|
||||
link_active=alias.transfer_token_expiration is not None
|
||||
and alias.transfer_token_expiration > arrow.utcnow(),
|
||||
)
|
||||
|
||||
|
||||
|
@ -134,12 +144,27 @@ def alias_transfer_receive_route():
|
|||
URL has ?alias_id=signed_alias_id
|
||||
"""
|
||||
token = request.args.get("token")
|
||||
alias = Alias.get_by(transfer_token=token)
|
||||
if not token:
|
||||
flash("Invalid transfer token", "error")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
hashed_token = hmac_alias_transfer_token(token)
|
||||
# TODO: Don't allow unhashed tokens once all the tokens have been migrated to the new format
|
||||
alias = Alias.get_by(transfer_token=token) or Alias.get_by(
|
||||
transfer_token=hashed_token
|
||||
)
|
||||
|
||||
if not alias:
|
||||
flash("Invalid link", "error")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
# TODO: Don't allow none once all the tokens have been migrated to the new format
|
||||
if (
|
||||
alias.transfer_token_expiration is not None
|
||||
and alias.transfer_token_expiration < arrow.utcnow()
|
||||
):
|
||||
flash("Expired link, please request a new one", "error")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
# alias already belongs to this user
|
||||
if alias.user_id == current_user.id:
|
||||
flash("You already own this alias", "warning")
|
||||
|
@ -176,11 +201,12 @@ def alias_transfer_receive_route():
|
|||
return redirect(request.url)
|
||||
|
||||
LOG.d(
|
||||
"transfer alias %s from %s to %s with %s",
|
||||
"transfer alias %s from %s to %s with %s with token %s",
|
||||
alias,
|
||||
alias.user,
|
||||
current_user,
|
||||
mailboxes,
|
||||
token,
|
||||
)
|
||||
transfer(alias, current_user, mailboxes)
|
||||
flash(f"You are now owner of {alias.email}", "success")
|
||||
|
|
|
@ -1286,6 +1286,9 @@ class Alias(Base, ModelMixin):
|
|||
|
||||
# used to transfer an alias to another user
|
||||
transfer_token = sa.Column(sa.String(64), default=None, unique=True, nullable=True)
|
||||
transfer_token_expiration = sa.Column(
|
||||
ArrowType, default=arrow.utcnow, nullable=True
|
||||
)
|
||||
|
||||
# have I been pwned
|
||||
hibp_last_check = sa.Column(ArrowType, default=None)
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
"""Add alias transfer token expiration
|
||||
|
||||
Revision ID: a7bcb872c12a
|
||||
Revises: 36646e5dc6d9
|
||||
Create Date: 2022-06-13 10:29:39.614171
|
||||
|
||||
"""
|
||||
import sqlalchemy_utils
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a7bcb872c12a'
|
||||
down_revision = '36646e5dc6d9'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('alias', sa.Column('transfer_token_expiration', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('alias', 'transfer_token_expiration')
|
||||
# ### end Alembic commands ###
|
|
@ -16,10 +16,6 @@
|
|||
emails.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In order to transfer ownership,
|
||||
please create the <b>Share URL</b> 👇 and send it to the other person.
|
||||
</p>
|
||||
|
||||
{% if alias_transfer_url %}
|
||||
<em data-toggle="tooltip"
|
||||
|
@ -29,18 +25,40 @@
|
|||
{{ alias_transfer_url }}
|
||||
</em>
|
||||
|
||||
<p class="mt-5">
|
||||
Please copy the transfer URL. <strong>We won't be able to display it again</strong>. If you need to access it again you can generate a new URL.
|
||||
</p>
|
||||
|
||||
<p class="mt-2">
|
||||
This transfer URL is <strong>valid for 24 hours</strong>. If it hasn't been used by then it will be automatically disabled.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="form-name" value="remove">
|
||||
<button class="btn btn-warning mt-2">Remove Share URL</button>
|
||||
<button class="btn btn-warning mt-2">Remove alias transfer URL</button>
|
||||
<div class="small-text">
|
||||
If you don't want to share this alias anymore, you can remove the share URL.
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post">
|
||||
<input type="hidden" name="form-name" value="create">
|
||||
<button class="btn btn-primary mt-2">Create Share URL</button>
|
||||
</form>
|
||||
{% if link_active %}
|
||||
<p class="alert alert-info">
|
||||
You have an active transfer link. If you don't want to share this alias any more, please delete the link.
|
||||
</p>
|
||||
<form method="post">
|
||||
<input type="hidden" name="form-name" value="remove">
|
||||
<button class="btn btn-warning mt-2">Remove alias transfer URL</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>
|
||||
In order to transfer ownership,
|
||||
please create the <b>Share URL</b> 👇 and send it to the other person.
|
||||
</p>
|
||||
<form method="post">
|
||||
<input type="hidden" name="form-name" value="create">
|
||||
<button class="btn btn-primary mt-2">Generate a new alias transfer URL</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<p class="mt-5">
|
||||
|
|
Loading…
Reference in a new issue