Merge pull request #199 from developStorm/webauthn-multiple-keys
Support Multiple Keys for WebAuthn
This commit is contained in:
commit
eb60028b1f
|
@ -22,10 +22,12 @@
|
||||||
<form id="formRegisterKey" method="post">
|
<form id="formRegisterKey" method="post">
|
||||||
{{ fido_token_form.csrf_token }}
|
{{ fido_token_form.csrf_token }}
|
||||||
{{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
|
{{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<button id="btnVerifyKey" class="btn btn-success mt-2" onclick="verifyKey();">Use your security key</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="text-center">
|
|
||||||
<button id="btnVerifyKey" class="btn btn-success mt-2">Use your security key</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if enable_otp %}
|
{% if enable_otp %}
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -68,7 +70,6 @@
|
||||||
$('#formRegisterKey').submit();
|
$('#formRegisterKey').submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#btnVerifyKey").click(verifyKey);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% if auto_activate %}
|
{% if auto_activate %}
|
||||||
|
|
|
@ -12,7 +12,7 @@ from app.config import MFA_USER_ID
|
||||||
from app.config import RP_ID, URL
|
from app.config import RP_ID, URL
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import User
|
from app.models import User, Fido
|
||||||
|
|
||||||
|
|
||||||
class FidoTokenForm(FlaskForm):
|
class FidoTokenForm(FlaskForm):
|
||||||
|
@ -40,17 +40,6 @@ def fido():
|
||||||
|
|
||||||
next_url = request.args.get("next")
|
next_url = request.args.get("next")
|
||||||
|
|
||||||
webauthn_user = webauthn.WebAuthnUser(
|
|
||||||
user.fido_uuid,
|
|
||||||
user.email,
|
|
||||||
user.name if user.name else user.email,
|
|
||||||
False,
|
|
||||||
user.fido_credential_id,
|
|
||||||
user.fido_pk,
|
|
||||||
user.fido_sign_count,
|
|
||||||
RP_ID,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Handling POST requests
|
# Handling POST requests
|
||||||
if fido_token_form.validate_on_submit():
|
if fido_token_form.validate_on_submit():
|
||||||
try:
|
try:
|
||||||
|
@ -61,11 +50,23 @@ def fido():
|
||||||
|
|
||||||
challenge = session["fido_challenge"]
|
challenge = session["fido_challenge"]
|
||||||
|
|
||||||
webauthn_assertion_response = webauthn.WebAuthnAssertionResponse(
|
|
||||||
webauthn_user, sk_assertion, challenge, URL, uv_required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
fido_key = Fido.get_by(
|
||||||
|
uuid=user.fido_uuid, credential_id=sk_assertion["id"]
|
||||||
|
)
|
||||||
|
webauthn_user = webauthn.WebAuthnUser(
|
||||||
|
user.fido_uuid,
|
||||||
|
user.email,
|
||||||
|
user.name if user.name else user.email,
|
||||||
|
False,
|
||||||
|
fido_key.credential_id,
|
||||||
|
fido_key.public_key,
|
||||||
|
fido_key.sign_count,
|
||||||
|
RP_ID,
|
||||||
|
)
|
||||||
|
webauthn_assertion_response = webauthn.WebAuthnAssertionResponse(
|
||||||
|
webauthn_user, sk_assertion, challenge, URL, uv_required=False
|
||||||
|
)
|
||||||
new_sign_count = webauthn_assertion_response.verify()
|
new_sign_count = webauthn_assertion_response.verify()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"An error occurred in WebAuthn verification process: {e}")
|
LOG.error(f"An error occurred in WebAuthn verification process: {e}")
|
||||||
|
@ -93,8 +94,24 @@ def fido():
|
||||||
|
|
||||||
session["fido_challenge"] = challenge.rstrip("=")
|
session["fido_challenge"] = challenge.rstrip("=")
|
||||||
|
|
||||||
|
fidos = Fido.filter_by(uuid=user.fido_uuid).all()
|
||||||
|
webauthn_users = []
|
||||||
|
for fido in fidos:
|
||||||
|
webauthn_users.append(
|
||||||
|
webauthn.WebAuthnUser(
|
||||||
|
user.fido_uuid,
|
||||||
|
user.email,
|
||||||
|
user.name if user.name else user.email,
|
||||||
|
False,
|
||||||
|
fido.credential_id,
|
||||||
|
fido.public_key,
|
||||||
|
fido.sign_count,
|
||||||
|
RP_ID,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions(
|
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions(
|
||||||
webauthn_user, challenge
|
webauthn_users, challenge
|
||||||
)
|
)
|
||||||
webauthn_assertion_options = webauthn_assertion_options.assertion_dict
|
webauthn_assertion_options = webauthn_assertion_options.assertion_dict
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,11 @@ from .views import (
|
||||||
api_key,
|
api_key,
|
||||||
custom_domain,
|
custom_domain,
|
||||||
alias_contact_manager,
|
alias_contact_manager,
|
||||||
|
enter_sudo,
|
||||||
mfa_setup,
|
mfa_setup,
|
||||||
mfa_cancel,
|
mfa_cancel,
|
||||||
fido_setup,
|
fido_setup,
|
||||||
fido_cancel,
|
fido_manage,
|
||||||
domain_detail,
|
domain_detail,
|
||||||
lifetime_licence,
|
lifetime_licence,
|
||||||
directory,
|
directory,
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
{% extends 'default.html' %}
|
{% extends 'default.html' %}
|
||||||
{% set active_page = "setting" %}
|
{% set active_page = "setting" %}
|
||||||
{% block title %}
|
{% block title %}
|
||||||
Unlink Security Key
|
SUDO MODE
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block default_content %}
|
{% block default_content %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h1 class="h2">Unlink Your Security Key</h1>
|
<h1 class="h2">Entering Sudo Mode</h1>
|
||||||
|
<p>
|
||||||
|
You are trying to change sensitive settings
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Please enter the password of your account so that we can ensure it's you.
|
Please enter the password of your account so that we can ensure it's you.
|
||||||
</p>
|
</p>
|
||||||
|
@ -20,7 +23,7 @@
|
||||||
|
|
||||||
{{ password_check_form.password(class="form-control", autofocus="true") }}
|
{{ password_check_form.password(class="form-control", autofocus="true") }}
|
||||||
{{ render_field_errors(password_check_form.password) }}
|
{{ render_field_errors(password_check_form.password) }}
|
||||||
<button class="btn btn-lg btn-danger mt-2">Unlink Key</button>
|
<button class="btn btn-lg btn-danger mt-2">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
|
@ -0,0 +1,53 @@
|
||||||
|
{% extends 'default.html' %}
|
||||||
|
{% set active_page = "setting" %}
|
||||||
|
{% block title %}
|
||||||
|
Manage Security Key
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="{{ url_for('static', filename='node_modules/qrious/dist/qrious.min.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block default_content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="h2">Manage Your Security Key</h1>
|
||||||
|
<p>Unlink all keys will also disable WebAuthn 2FA.</p>
|
||||||
|
|
||||||
|
<form id="formManageKey" method="post">
|
||||||
|
{{ fido_manage_form.csrf_token }}
|
||||||
|
{{ fido_manage_form.credential_id(class="form-control", placeholder="") }}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">ID</th>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col">Linked At</th>
|
||||||
|
<th scope="col" class="text-center">Operation</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{%for key in keys%}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{ key.id }}</th>
|
||||||
|
<td>{{ key.name }}</td>
|
||||||
|
<td>{{key.created_at | dt}}</td>
|
||||||
|
<td class="text-center"><button class="btn btn-outline-danger" onclick="$('#credential_id').val('{{ key.credential_id }}'); $('#formManageKey').submit();">Unlink</button></td>
|
||||||
|
</tr>
|
||||||
|
{%endfor%}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">#</th>
|
||||||
|
<td>Link a New Key</td>
|
||||||
|
<td></td>
|
||||||
|
<td class="text-center"><a href="{{ url_for('dashboard.fido_setup') }}"><button class="btn btn-outline-success">Link</button></a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -19,10 +19,14 @@
|
||||||
<form id="formRegisterKey" method="post">
|
<form id="formRegisterKey" method="post">
|
||||||
{{ fido_token_form.csrf_token }}
|
{{ fido_token_form.csrf_token }}
|
||||||
{{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
|
{{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
|
||||||
|
|
||||||
|
{{ fido_token_form.key_name(class="form-control", placeholder="Name of your key (Required)") }}
|
||||||
|
{{ render_field_errors(fido_token_form.key_name) }}
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<button id="btnRegisterKey" class="btn btn-lg btn-primary mt-2" onclick="registerKey();">Register Key</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="text-center">
|
|
||||||
<button id="btnRegisterKey" class="btn btn-lg btn-primary mt-2">Register Key</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function registerKey() {
|
async function registerKey() {
|
||||||
|
@ -33,6 +37,7 @@
|
||||||
JSON.parse('{{credential_create_options|tojson|safe}}')
|
JSON.parse('{{credential_create_options|tojson|safe}}')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
let credential
|
let credential
|
||||||
try {
|
try {
|
||||||
credential = await navigator.credentials.create({
|
credential = await navigator.credentials.create({
|
||||||
|
@ -51,8 +56,6 @@
|
||||||
$('#formRegisterKey').submit();
|
$('#formRegisterKey').submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#btnRegisterKey").click(registerKey);
|
|
||||||
$('document').ready(registerKey());
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -97,7 +97,7 @@
|
||||||
{% if current_user.fido_uuid is none %}
|
{% if current_user.fido_uuid is none %}
|
||||||
<a href="{{ url_for('dashboard.fido_setup') }}" class="btn btn-outline-primary">Setup WebAuthn</a>
|
<a href="{{ url_for('dashboard.fido_setup') }}" class="btn btn-outline-primary">Setup WebAuthn</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('dashboard.fido_cancel') }}" class="btn btn-outline-danger">Disable WebAuthn</a>
|
<a href="{{ url_for('dashboard.fido_manage') }}" class="btn btn-outline-info">Manage WebAuthn</a>
|
||||||
<a href="{{ url_for('dashboard.recovery_code_route') }}" class="btn btn-outline-secondary">Recovery Codes</a>
|
<a href="{{ url_for('dashboard.recovery_code_route') }}" class="btn btn-outline-secondary">Recovery Codes</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from flask import render_template, flash, redirect, url_for, session, request
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import PasswordField, validators
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from app.dashboard.base import dashboard_bp
|
||||||
|
from app.log import LOG
|
||||||
|
|
||||||
|
_SUDO_GAP = 900
|
||||||
|
|
||||||
|
|
||||||
|
class LoginForm(FlaskForm):
|
||||||
|
password = PasswordField("Password", validators=[validators.DataRequired()])
|
||||||
|
|
||||||
|
|
||||||
|
@dashboard_bp.route("/enter_sudo", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def enter_sudo():
|
||||||
|
password_check_form = LoginForm()
|
||||||
|
|
||||||
|
if password_check_form.validate_on_submit():
|
||||||
|
password = password_check_form.password.data
|
||||||
|
|
||||||
|
if current_user.check_password(password):
|
||||||
|
session["sudo_time"] = int(time())
|
||||||
|
|
||||||
|
# User comes to sudo page from another page
|
||||||
|
next_url = request.args.get("next")
|
||||||
|
if next_url:
|
||||||
|
LOG.debug("redirect user to %s", next_url)
|
||||||
|
return redirect(next_url)
|
||||||
|
else:
|
||||||
|
LOG.debug("redirect user to dashboard")
|
||||||
|
return redirect(url_for("dashboard.index"))
|
||||||
|
else:
|
||||||
|
flash("Incorrect password", "warning")
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"dashboard/enter_sudo.html", password_check_form=password_check_form
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sudo_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrap(*args, **kwargs):
|
||||||
|
if (
|
||||||
|
"sudo_time" not in session
|
||||||
|
or (time() - int(session["sudo_time"])) > _SUDO_GAP
|
||||||
|
):
|
||||||
|
return redirect(url_for("dashboard.enter_sudo", next=request.path))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrap
|
|
@ -1,45 +0,0 @@
|
||||||
from flask import render_template, flash, redirect, url_for
|
|
||||||
from flask_login import login_required, current_user
|
|
||||||
from flask_wtf import FlaskForm
|
|
||||||
from wtforms import PasswordField, validators
|
|
||||||
|
|
||||||
from app.dashboard.base import dashboard_bp
|
|
||||||
from app.extensions import db
|
|
||||||
from app.models import RecoveryCode
|
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(FlaskForm):
|
|
||||||
password = PasswordField("Password", validators=[validators.DataRequired()])
|
|
||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/fido_cancel", methods=["GET", "POST"])
|
|
||||||
@login_required
|
|
||||||
def fido_cancel():
|
|
||||||
if not current_user.fido_enabled():
|
|
||||||
flash("You haven't registed a security key", "warning")
|
|
||||||
return redirect(url_for("dashboard.index"))
|
|
||||||
|
|
||||||
password_check_form = LoginForm()
|
|
||||||
|
|
||||||
if password_check_form.validate_on_submit():
|
|
||||||
password = password_check_form.password.data
|
|
||||||
|
|
||||||
if current_user.check_password(password):
|
|
||||||
current_user.fido_pk = None
|
|
||||||
current_user.fido_uuid = None
|
|
||||||
current_user.fido_sign_count = None
|
|
||||||
current_user.fido_credential_id = None
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# user does not have any 2FA enabled left, delete all recovery codes
|
|
||||||
if not current_user.two_factor_authentication_enabled():
|
|
||||||
RecoveryCode.empty(current_user)
|
|
||||||
|
|
||||||
flash("We've unlinked your security key.", "success")
|
|
||||||
return redirect(url_for("dashboard.index"))
|
|
||||||
else:
|
|
||||||
flash("Incorrect password", "warning")
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
"dashboard/fido_cancel.html", password_check_form=password_check_form
|
|
||||||
)
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
from flask import render_template, flash, redirect, url_for
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import HiddenField, validators
|
||||||
|
|
||||||
|
from app.dashboard.base import dashboard_bp
|
||||||
|
from app.extensions import db
|
||||||
|
from app.log import LOG
|
||||||
|
from app.models import RecoveryCode, Fido
|
||||||
|
from app.dashboard.views.enter_sudo import sudo_required
|
||||||
|
|
||||||
|
|
||||||
|
class FidoManageForm(FlaskForm):
|
||||||
|
credential_id = HiddenField("credential_id", validators=[validators.DataRequired()])
|
||||||
|
|
||||||
|
|
||||||
|
@dashboard_bp.route("/fido_manage", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
@sudo_required
|
||||||
|
def fido_manage():
|
||||||
|
if not current_user.fido_enabled():
|
||||||
|
flash("You haven't registered a security key", "warning")
|
||||||
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
|
fido_manage_form = FidoManageForm()
|
||||||
|
|
||||||
|
if fido_manage_form.validate_on_submit():
|
||||||
|
credential_id = fido_manage_form.credential_id.data
|
||||||
|
|
||||||
|
fido_key = Fido.get_by(uuid=current_user.fido_uuid, credential_id=credential_id)
|
||||||
|
|
||||||
|
if not fido_key:
|
||||||
|
flash("Unknown error, redirect back to manage page", "warning")
|
||||||
|
return redirect(url_for("dashboard.fido_manage"))
|
||||||
|
|
||||||
|
Fido.delete(fido_key.id)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
LOG.d(f"FIDO Key ID={fido_key.id} Removed")
|
||||||
|
flash(f"Key {fido_key.name} successfully unlinked", "success")
|
||||||
|
|
||||||
|
# Disable FIDO for the user if all keys have been deleted
|
||||||
|
if not Fido.filter_by(uuid=current_user.fido_uuid).all():
|
||||||
|
current_user.fido_uuid = None
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# user does not have any 2FA enabled left, delete all recovery codes
|
||||||
|
if not current_user.two_factor_authentication_enabled():
|
||||||
|
RecoveryCode.empty(current_user)
|
||||||
|
|
||||||
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
|
return redirect(url_for("dashboard.fido_manage"))
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"dashboard/fido_manage.html",
|
||||||
|
fido_manage_form=fido_manage_form,
|
||||||
|
keys=Fido.filter_by(uuid=current_user.fido_uuid),
|
||||||
|
)
|
|
@ -1,30 +1,31 @@
|
||||||
import json
|
import json
|
||||||
import secrets
|
import secrets
|
||||||
import uuid
|
import uuid
|
||||||
|
from time import time
|
||||||
|
|
||||||
import webauthn
|
import webauthn
|
||||||
from flask import render_template, flash, redirect, url_for, session
|
from flask import render_template, flash, redirect, url_for, session
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import HiddenField, validators
|
from wtforms import StringField, HiddenField, validators
|
||||||
|
|
||||||
from app.config import RP_ID, URL
|
from app.config import RP_ID, URL
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
|
from app.models import Fido, RecoveryCode
|
||||||
|
from app.dashboard.views.enter_sudo import sudo_required
|
||||||
|
|
||||||
|
|
||||||
class FidoTokenForm(FlaskForm):
|
class FidoTokenForm(FlaskForm):
|
||||||
|
key_name = StringField("key_name", validators=[validators.DataRequired()])
|
||||||
sk_assertion = HiddenField("sk_assertion", validators=[validators.DataRequired()])
|
sk_assertion = HiddenField("sk_assertion", validators=[validators.DataRequired()])
|
||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/fido_setup", methods=["GET", "POST"])
|
@dashboard_bp.route("/fido_setup", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
|
@sudo_required
|
||||||
def fido_setup():
|
def fido_setup():
|
||||||
if current_user.fido_enabled():
|
|
||||||
flash("You have already registered your security key", "warning")
|
|
||||||
return redirect(url_for("dashboard.index"))
|
|
||||||
|
|
||||||
if not current_user.can_use_fido:
|
if not current_user.can_use_fido:
|
||||||
flash(
|
flash(
|
||||||
"This feature is currently in invitation-only beta. Please send us an email if you want to try",
|
"This feature is currently in invitation-only beta. Please send us an email if you want to try",
|
||||||
|
@ -32,6 +33,11 @@ def fido_setup():
|
||||||
)
|
)
|
||||||
return redirect(url_for("dashboard.index"))
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
|
if current_user.fido_uuid is not None:
|
||||||
|
fidos = Fido.filter_by(uuid=current_user.fido_uuid).all()
|
||||||
|
else:
|
||||||
|
fidos = []
|
||||||
|
|
||||||
fido_token_form = FidoTokenForm()
|
fido_token_form = FidoTokenForm()
|
||||||
|
|
||||||
# Handling POST requests
|
# Handling POST requests
|
||||||
|
@ -61,17 +67,32 @@ def fido_setup():
|
||||||
flash("Key registration failed.", "warning")
|
flash("Key registration failed.", "warning")
|
||||||
return redirect(url_for("dashboard.index"))
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
current_user.fido_pk = str(fido_credential.public_key, "utf-8")
|
if current_user.fido_uuid is None:
|
||||||
current_user.fido_uuid = fido_uuid
|
current_user.fido_uuid = fido_uuid
|
||||||
current_user.fido_sign_count = fido_credential.sign_count
|
|
||||||
current_user.fido_credential_id = str(fido_credential.credential_id, "utf-8")
|
Fido.create(
|
||||||
|
credential_id=str(fido_credential.credential_id, "utf-8"),
|
||||||
|
uuid=fido_uuid,
|
||||||
|
public_key=str(fido_credential.public_key, "utf-8"),
|
||||||
|
sign_count=fido_credential.sign_count,
|
||||||
|
name=fido_token_form.key_name.data,
|
||||||
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
LOG.d(
|
||||||
|
f"credential_id={str(fido_credential.credential_id, 'utf-8')} added for {fido_uuid}"
|
||||||
|
)
|
||||||
|
|
||||||
flash("Security key has been activated", "success")
|
flash("Security key has been activated", "success")
|
||||||
return redirect(url_for("dashboard.recovery_code_route"))
|
if not RecoveryCode.query.filter_by(user_id=current_user.id).all():
|
||||||
|
return redirect(url_for("dashboard.recovery_code_route"))
|
||||||
|
else:
|
||||||
|
return redirect(url_for("dashboard.fido_manage"))
|
||||||
|
|
||||||
# Prepare information for key registration process
|
# Prepare information for key registration process
|
||||||
fido_uuid = str(uuid.uuid4())
|
fido_uuid = (
|
||||||
|
str(uuid.uuid4()) if current_user.fido_uuid is None else current_user.fido_uuid
|
||||||
|
)
|
||||||
challenge = secrets.token_urlsafe(32)
|
challenge = secrets.token_urlsafe(32)
|
||||||
|
|
||||||
credential_create_options = webauthn.WebAuthnMakeCredentialOptions(
|
credential_create_options = webauthn.WebAuthnMakeCredentialOptions(
|
||||||
|
@ -91,6 +112,16 @@ def fido_setup():
|
||||||
registration_dict = credential_create_options.registration_dict
|
registration_dict = credential_create_options.registration_dict
|
||||||
del registration_dict["extensions"]["webauthn.loc"]
|
del registration_dict["extensions"]["webauthn.loc"]
|
||||||
|
|
||||||
|
# Prevent user from adding duplicated keys
|
||||||
|
for fido in fidos:
|
||||||
|
registration_dict["excludeCredentials"].append(
|
||||||
|
{
|
||||||
|
"type": "public-key",
|
||||||
|
"id": fido.credential_id,
|
||||||
|
"transports": ["usb", "nfc", "ble", "internal"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
session["fido_uuid"] = fido_uuid
|
session["fido_uuid"] = fido_uuid
|
||||||
session["fido_challenge"] = challenge.rstrip("=")
|
session["fido_challenge"] = challenge.rstrip("=")
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from wtforms import StringField, validators
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import RecoveryCode
|
from app.models import RecoveryCode
|
||||||
|
from app.dashboard.views.enter_sudo import sudo_required
|
||||||
|
|
||||||
|
|
||||||
class OtpTokenForm(FlaskForm):
|
class OtpTokenForm(FlaskForm):
|
||||||
|
@ -15,6 +16,7 @@ class OtpTokenForm(FlaskForm):
|
||||||
|
|
||||||
@dashboard_bp.route("/mfa_cancel", methods=["GET", "POST"])
|
@dashboard_bp.route("/mfa_cancel", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
|
@sudo_required
|
||||||
def mfa_cancel():
|
def mfa_cancel():
|
||||||
if not current_user.enable_otp:
|
if not current_user.enable_otp:
|
||||||
flash("you don't have MFA enabled", "warning")
|
flash("you don't have MFA enabled", "warning")
|
||||||
|
|
|
@ -7,6 +7,7 @@ from wtforms import StringField, validators
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
|
from app.dashboard.views.enter_sudo import sudo_required
|
||||||
|
|
||||||
|
|
||||||
class OtpTokenForm(FlaskForm):
|
class OtpTokenForm(FlaskForm):
|
||||||
|
@ -15,6 +16,7 @@ class OtpTokenForm(FlaskForm):
|
||||||
|
|
||||||
@dashboard_bp.route("/mfa_setup", methods=["GET", "POST"])
|
@dashboard_bp.route("/mfa_setup", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
|
@sudo_required
|
||||||
def mfa_setup():
|
def mfa_setup():
|
||||||
if current_user.enable_otp:
|
if current_user.enable_otp:
|
||||||
flash("you have already enabled MFA", "warning")
|
flash("you have already enabled MFA", "warning")
|
||||||
|
|
|
@ -121,6 +121,19 @@ class AliasGeneratorEnum(EnumE):
|
||||||
uuid = 2 # aliases are generated based on uuid
|
uuid = 2 # aliases are generated based on uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Fido(db.Model, ModelMixin):
|
||||||
|
__tablename__ = "fido"
|
||||||
|
credential_id = db.Column(db.String(), nullable=False, unique=True, index=True)
|
||||||
|
uuid = db.Column(
|
||||||
|
db.ForeignKey("users.fido_uuid", ondelete="cascade"),
|
||||||
|
unique=False,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
public_key = db.Column(db.String(), nullable=False, unique=True)
|
||||||
|
sign_count = db.Column(db.Integer(), nullable=False)
|
||||||
|
name = db.Column(db.String(128), nullable=False, unique=False)
|
||||||
|
|
||||||
|
|
||||||
class User(db.Model, ModelMixin, UserMixin):
|
class User(db.Model, ModelMixin, UserMixin):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
email = db.Column(db.String(256), unique=True, nullable=False)
|
email = db.Column(db.String(256), unique=True, nullable=False)
|
||||||
|
@ -151,9 +164,6 @@ class User(db.Model, ModelMixin, UserMixin):
|
||||||
|
|
||||||
# Fields for WebAuthn
|
# Fields for WebAuthn
|
||||||
fido_uuid = db.Column(db.String(), nullable=True, unique=True)
|
fido_uuid = db.Column(db.String(), nullable=True, unique=True)
|
||||||
fido_credential_id = db.Column(db.String(), nullable=True, unique=True)
|
|
||||||
fido_pk = db.Column(db.String(), nullable=True, unique=True)
|
|
||||||
fido_sign_count = db.Column(db.Integer(), nullable=True)
|
|
||||||
|
|
||||||
# whether user can use Fido
|
# whether user can use Fido
|
||||||
can_use_fido = db.Column(
|
can_use_fido = db.Column(
|
||||||
|
|
|
@ -38,6 +38,7 @@ from app.log import LOG
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Client,
|
Client,
|
||||||
User,
|
User,
|
||||||
|
Fido,
|
||||||
ClientUser,
|
ClientUser,
|
||||||
Alias,
|
Alias,
|
||||||
RedirectUri,
|
RedirectUri,
|
||||||
|
@ -143,8 +144,10 @@ def fake_data():
|
||||||
otp_secret="base32secret3232",
|
otp_secret="base32secret3232",
|
||||||
can_use_fido=True,
|
can_use_fido=True,
|
||||||
intro_shown=True,
|
intro_shown=True,
|
||||||
|
fido_uuid=None,
|
||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
user.trial_end = None
|
user.trial_end = None
|
||||||
|
|
||||||
LifetimeCoupon.create(code="coupon", nb_used=10)
|
LifetimeCoupon.create(code="coupon", nb_used=10)
|
||||||
|
|
|
@ -56,7 +56,7 @@ const transformCredentialRequestOptions = (
|
||||||
const transformCredentialCreateOptions = (
|
const transformCredentialCreateOptions = (
|
||||||
credentialCreateOptionsFromServer
|
credentialCreateOptionsFromServer
|
||||||
) => {
|
) => {
|
||||||
let { challenge, user } = credentialCreateOptionsFromServer;
|
let { challenge, user, excludeCredentials } = credentialCreateOptionsFromServer;
|
||||||
user.id = Uint8Array.from(
|
user.id = Uint8Array.from(
|
||||||
atob(
|
atob(
|
||||||
credentialCreateOptionsFromServer.user.id
|
credentialCreateOptionsFromServer.user.id
|
||||||
|
@ -75,10 +75,17 @@ const transformCredentialCreateOptions = (
|
||||||
(c) => c.charCodeAt(0)
|
(c) => c.charCodeAt(0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
excludeCredentials = excludeCredentials.map((credentialDescriptor) => {
|
||||||
|
let { id } = credentialDescriptor;
|
||||||
|
id = id.replace(/\_/g, "/").replace(/\-/g, "+");
|
||||||
|
id = Uint8Array.from(atob(id), (c) => c.charCodeAt(0));
|
||||||
|
return Object.assign({}, credentialDescriptor, { id });
|
||||||
|
});
|
||||||
|
|
||||||
const transformedCredentialCreateOptions = Object.assign(
|
const transformedCredentialCreateOptions = Object.assign(
|
||||||
{},
|
{},
|
||||||
credentialCreateOptionsFromServer,
|
credentialCreateOptionsFromServer,
|
||||||
{ challenge, user }
|
{ challenge, user, excludeCredentials }
|
||||||
);
|
);
|
||||||
|
|
||||||
return transformedCredentialCreateOptions;
|
return transformedCredentialCreateOptions;
|
||||||
|
|
Loading…
Reference in New Issue