mirror of
https://github.com/simple-login/app.git
synced 2024-09-21 07:25:59 +08:00
commit
e35fb631cf
|
@ -11,5 +11,6 @@ from .views import (
|
|||
facebook,
|
||||
change_email,
|
||||
mfa,
|
||||
fido,
|
||||
social,
|
||||
)
|
||||
|
|
66
app/auth/templates/auth/fido.html
Normal file
66
app/auth/templates/auth/fido.html
Normal file
|
@ -0,0 +1,66 @@
|
|||
{% extends "single.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
Verify Your Security Key
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{{ url_for('static', filename='assets/js/vendors/base64.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='assets/js/vendors/webauthn.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block single_content %}
|
||||
<div class="bg-white p-6" style="margin: auto">
|
||||
|
||||
<div class="mb-2">
|
||||
Your account is protected with your security key (WebAuthn). <br><br>
|
||||
Follow your browser's steps to continue the sign-in process.
|
||||
</div>
|
||||
|
||||
<form id="formRegisterKey" method="post">
|
||||
{{ fido_token_form.csrf_token }}
|
||||
{{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
|
||||
</form>
|
||||
<div class="text-center">
|
||||
<button id="btnVerifyKey" class="btn btn-success mt-2">Use your security key</button>
|
||||
</div>
|
||||
|
||||
{% if enable_otp %}
|
||||
<div class="text-center text-muted mb-6" style="margin-top: 1em;">
|
||||
Don't have your key with you? <br> <a href="{{ url_for('auth.mfa') }}">Verify by One-Time Password</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
async function verifyKey () {
|
||||
$("#btnVerifyKey").prop('disabled', true);
|
||||
$("#btnVerifyKey").text('Waiting for Security Key...');
|
||||
|
||||
const credentialRequestOptions = transformCredentialRequestOptions(
|
||||
JSON.parse('{{webauthn_assertion_options|tojson|safe}}')
|
||||
)
|
||||
|
||||
let assertion;
|
||||
try {
|
||||
assertion = await navigator.credentials.get({
|
||||
publicKey: credentialRequestOptions
|
||||
});
|
||||
} catch (err) {
|
||||
toastr.error("An error occurred when we trying to verify your key.");
|
||||
$("#btnVerifyKey").prop('disabled', false);
|
||||
$("#btnVerifyKey").text('Use your security key');
|
||||
return console.error("Error when trying to get credential:", err);
|
||||
}
|
||||
|
||||
const skAssertion = transformAssertionForServer(assertion);
|
||||
$('#sk_assertion').val(JSON.stringify(skAssertion));
|
||||
$('#formRegisterKey').submit();
|
||||
}
|
||||
|
||||
$("#btnVerifyKey").click(verifyKey);
|
||||
</script>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -28,6 +28,12 @@
|
|||
<button class="btn btn-success mt-2">Validate</button>
|
||||
</form>
|
||||
|
||||
{% if enable_fido %}
|
||||
<div class="text-center text-muted mb-6" style="margin-top: 1em;">
|
||||
Having trouble with your authenticator? <br> <a href="{{ url_for('auth.fido') }}">Verify by your security key</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
104
app/auth/views/fido.py
Normal file
104
app/auth/views/fido.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
import json
|
||||
import secrets
|
||||
import webauthn
|
||||
from app.config import RP_ID, URL
|
||||
|
||||
from flask import request, render_template, redirect, url_for, flash, session
|
||||
from flask_login import login_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import HiddenField, validators
|
||||
|
||||
from app.auth.base import auth_bp
|
||||
from app.config import MFA_USER_ID
|
||||
from app.log import LOG
|
||||
from app.models import User
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class FidoTokenForm(FlaskForm):
|
||||
sk_assertion = HiddenField("sk_assertion", validators=[validators.DataRequired()])
|
||||
|
||||
|
||||
@auth_bp.route("/fido", methods=["GET", "POST"])
|
||||
def fido():
|
||||
# passed from login page
|
||||
user_id = session.get(MFA_USER_ID)
|
||||
|
||||
# user access this page directly without passing by login page
|
||||
if not user_id:
|
||||
flash("Unknown error, redirect back to main page", "warning")
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
user = User.get(user_id)
|
||||
|
||||
if not (user and (user.fido_enabled())):
|
||||
flash("Only user with security key linked should go to this page", "warning")
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
fido_token_form = FidoTokenForm()
|
||||
|
||||
next_url = request.args.get("next")
|
||||
|
||||
webauthn_user = webauthn.WebAuthnUser(
|
||||
user.fido_uuid,
|
||||
user.email,
|
||||
user.name,
|
||||
False,
|
||||
user.fido_credential_id,
|
||||
user.fido_pk,
|
||||
user.fido_sign_count,
|
||||
RP_ID,
|
||||
)
|
||||
|
||||
# Handling POST requests
|
||||
if fido_token_form.validate_on_submit():
|
||||
try:
|
||||
sk_assertion = json.loads(fido_token_form.sk_assertion.data)
|
||||
except Exception as e:
|
||||
flash("Key verification failed. Error: Invalid Payload", "warning")
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
challenge = session["fido_challenge"]
|
||||
|
||||
webauthn_assertion_response = webauthn.WebAuthnAssertionResponse(
|
||||
webauthn_user, sk_assertion, challenge, URL, uv_required=False
|
||||
)
|
||||
|
||||
try:
|
||||
new_sign_count = webauthn_assertion_response.verify()
|
||||
except Exception as e:
|
||||
LOG.error(f"An error occurred in WebAuthn verification process: {e}")
|
||||
flash("Key verification failed.", "warning")
|
||||
else:
|
||||
user.fido_sign_count = new_sign_count
|
||||
db.session.commit()
|
||||
del session[MFA_USER_ID]
|
||||
|
||||
login_user(user)
|
||||
flash(f"Welcome back {user.name}!", "success")
|
||||
|
||||
# User comes to login page from another page
|
||||
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"))
|
||||
|
||||
# Prepare information for key registration process
|
||||
session.pop("challenge", None)
|
||||
challenge = secrets.token_urlsafe(32)
|
||||
|
||||
session["fido_challenge"] = challenge.rstrip("=")
|
||||
|
||||
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions(
|
||||
webauthn_user, challenge
|
||||
)
|
||||
webauthn_assertion_options = webauthn_assertion_options.assertion_dict
|
||||
|
||||
return render_template(
|
||||
"auth/fido.html",
|
||||
fido_token_form=fido_token_form,
|
||||
webauthn_assertion_options=webauthn_assertion_options,
|
||||
enable_otp=user.enable_otp,
|
||||
)
|
|
@ -14,7 +14,15 @@ def after_login(user, next_url):
|
|||
If user enables MFA: redirect user to MFA page
|
||||
Otherwise redirect to dashboard page if no next_url
|
||||
"""
|
||||
if user.enable_otp:
|
||||
if user.fido_enabled():
|
||||
# Use the same session for FIDO so that we can easily
|
||||
# switch between these two 2FA option
|
||||
session[MFA_USER_ID] = user.id
|
||||
if next_url:
|
||||
return redirect(url_for("auth.fido", next_url=next_url))
|
||||
else:
|
||||
return redirect(url_for("auth.fido"))
|
||||
elif user.enable_otp:
|
||||
session[MFA_USER_ID] = user.id
|
||||
if next_url:
|
||||
return redirect(url_for("auth.mfa", next_url=next_url))
|
||||
|
|
|
@ -55,4 +55,8 @@ def mfa():
|
|||
else:
|
||||
flash("Incorrect token", "warning")
|
||||
|
||||
return render_template("auth/mfa.html", otp_token_form=otp_token_form)
|
||||
return render_template(
|
||||
"auth/mfa.html",
|
||||
otp_token_form=otp_token_form,
|
||||
enable_fido=(user.fido_enabled()),
|
||||
)
|
||||
|
|
|
@ -4,6 +4,7 @@ import string
|
|||
import subprocess
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from urllib.parse import urlparse
|
||||
|
||||
SHA1 = subprocess.getoutput("git rev-parse HEAD")
|
||||
ROOT_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
@ -38,6 +39,9 @@ DEBUG = os.environ["DEBUG"] if "DEBUG" in os.environ else False
|
|||
URL = os.environ["URL"]
|
||||
print(">>> URL:", URL)
|
||||
|
||||
# Calculate RP_ID for WebAuthn
|
||||
RP_ID = urlparse(URL).hostname
|
||||
|
||||
SENTRY_DSN = os.environ.get("SENTRY_DSN")
|
||||
|
||||
# can use another sentry project for the front-end to avoid noises
|
||||
|
|
|
@ -11,6 +11,8 @@ from .views import (
|
|||
alias_contact_manager,
|
||||
mfa_setup,
|
||||
mfa_cancel,
|
||||
fido_setup,
|
||||
fido_cancel,
|
||||
domain_detail,
|
||||
lifetime_licence,
|
||||
directory,
|
||||
|
|
27
app/dashboard/templates/dashboard/fido_cancel.html
Normal file
27
app/dashboard/templates/dashboard/fido_cancel.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
{% extends 'default.html' %}
|
||||
{% set active_page = "setting" %}
|
||||
{% block title %}
|
||||
Unlink Security Key
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block default_content %}
|
||||
<div class="bg-white p-6" style="max-width: 60em; margin: auto">
|
||||
<h1 class="h2">Unlink Your Security Key</h1>
|
||||
<p>
|
||||
Please enter the password of your account so that we can ensure it's you.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
{{ password_check_form.csrf_token }}
|
||||
|
||||
<div class="font-weight-bold mt-5">Password</div>
|
||||
|
||||
{{ password_check_form.password(class="form-control", autofocus="true") }}
|
||||
{{ render_field_errors(password_check_form.password) }}
|
||||
<button class="btn btn-lg btn-danger mt-2">Unlink Key</button>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
58
app/dashboard/templates/dashboard/fido_setup.html
Normal file
58
app/dashboard/templates/dashboard/fido_setup.html
Normal file
|
@ -0,0 +1,58 @@
|
|||
{% extends 'default.html' %}
|
||||
{% set active_page = "setting" %}
|
||||
{% block title %}
|
||||
Security Key Setup
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{{ url_for('static', filename='node_modules/qrious/dist/qrious.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='assets/js/vendors/base64.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='assets/js/vendors/webauthn.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block default_content %}
|
||||
<div class="bg-white p-6" style="max-width: 60em; margin: auto">
|
||||
<h1 class="h2 text-center">Register Your Security Key</h1>
|
||||
<p class="text-center">Follow your browser's steps to register your security key with SimpleLogin</p>
|
||||
|
||||
<form id="formRegisterKey" method="post">
|
||||
{{ fido_token_form.csrf_token }}
|
||||
{{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
|
||||
</form>
|
||||
<div class="text-center">
|
||||
<button id="btnRegisterKey" class="btn btn-lg btn-primary mt-2">Register Key</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function registerKey () {
|
||||
$("#btnRegisterKey").prop('disabled', true);
|
||||
$("#btnRegisterKey").text('Waiting for Security Key...');
|
||||
|
||||
const pkCredentialCreateOptions = transformCredentialCreateOptions(
|
||||
JSON.parse('{{credential_create_options|tojson|safe}}')
|
||||
)
|
||||
|
||||
let credential
|
||||
try {
|
||||
credential = await navigator.credentials.create({
|
||||
publicKey: pkCredentialCreateOptions
|
||||
});
|
||||
} catch (err) {
|
||||
toastr.error("An error occurred when we trying to register your key.");
|
||||
$("#btnRegisterKey").prop('disabled', false);
|
||||
$("#btnRegisterKey").text('Register Key');
|
||||
return console.error("Error when trying to create credential:", err);
|
||||
}
|
||||
|
||||
const skAssertion = transformNewAssertionForServer(credential);
|
||||
|
||||
$('#sk_assertion').val(JSON.stringify(skAssertion));
|
||||
$('#formRegisterKey').submit();
|
||||
}
|
||||
|
||||
$("#btnRegisterKey").click(registerKey);
|
||||
$('document').ready(registerKey());
|
||||
</script>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -85,18 +85,32 @@
|
|||
</div>
|
||||
<!-- END change name & profile picture -->
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="card-title">Security Key (WebAuthn)</div>
|
||||
<div class="mb-3">
|
||||
You can secure your account by linking either your FIDO-supported physical key such as Yubikey, Google Titan,
|
||||
or a device with appropriate hardware to your account.
|
||||
</div>
|
||||
{% if current_user.fido_uuid is none %}
|
||||
<a href="{{ url_for('dashboard.fido_setup') }}" class="btn btn-outline-primary">Setup WebAuthn</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('dashboard.fido_cancel') }}" class="btn btn-outline-danger">Disable WebAuthn</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="card-title">Multi-Factor Authentication (MFA)</div>
|
||||
<div class="card-title">One-Time Password (TOTP)</div>
|
||||
<div class="mb-3">
|
||||
Secure your account with Multi-Factor Authentication. <br>
|
||||
Secure your account with One-Time Password. <br>
|
||||
This requires having applications like Google Authenticator, Authy, MyDigiPassword, etc.
|
||||
</div>
|
||||
{% if not current_user.enable_otp %}
|
||||
<a href="{{ url_for('dashboard.mfa_setup') }}" class="btn btn-outline-primary">Enable</a>
|
||||
<a href="{{ url_for('dashboard.mfa_setup') }}" class="btn btn-outline-primary">Setup TOTP</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('dashboard.mfa_cancel') }}" class="btn btn-outline-danger">Cancel MFA</a>
|
||||
<a href="{{ url_for('dashboard.mfa_cancel') }}" class="btn btn-outline-danger">Disable TOTP</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
39
app/dashboard/views/fido_cancel.py
Normal file
39
app/dashboard/views/fido_cancel.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
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
|
||||
|
||||
|
||||
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()
|
||||
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
|
||||
)
|
95
app/dashboard/views/fido_setup.py
Normal file
95
app/dashboard/views/fido_setup.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
import uuid
|
||||
import json
|
||||
import secrets
|
||||
import webauthn
|
||||
from app.config import RP_ID, URL
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from flask import render_template, flash, redirect, url_for, session
|
||||
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
|
||||
|
||||
|
||||
class FidoTokenForm(FlaskForm):
|
||||
sk_assertion = HiddenField("sk_assertion", validators=[validators.DataRequired()])
|
||||
|
||||
|
||||
@dashboard_bp.route("/fido_setup", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def fido_setup():
|
||||
if current_user.fido_enabled():
|
||||
flash("You have already registered your security key", "warning")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
fido_token_form = FidoTokenForm()
|
||||
|
||||
# Handling POST requests
|
||||
if fido_token_form.validate_on_submit():
|
||||
try:
|
||||
sk_assertion = json.loads(fido_token_form.sk_assertion.data)
|
||||
except Exception as e:
|
||||
flash("Key registration failed. Error: Invalid Payload", "warning")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
fido_uuid = session["fido_uuid"]
|
||||
challenge = session["fido_challenge"]
|
||||
|
||||
fido_reg_response = webauthn.WebAuthnRegistrationResponse(
|
||||
RP_ID,
|
||||
URL,
|
||||
sk_assertion,
|
||||
challenge,
|
||||
trusted_attestation_cert_required=False,
|
||||
none_attestation_permitted=True,
|
||||
)
|
||||
|
||||
try:
|
||||
fido_credential = fido_reg_response.verify()
|
||||
except Exception as e:
|
||||
LOG.error(f"An error occurred in WebAuthn registration process: {e}")
|
||||
flash("Key registration failed.", "warning")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
current_user.fido_pk = str(fido_credential.public_key, "utf-8")
|
||||
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")
|
||||
db.session.commit()
|
||||
|
||||
flash("Security key has been activated", "success")
|
||||
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
# Prepare information for key registration process
|
||||
fido_uuid = str(uuid.uuid4())
|
||||
challenge = secrets.token_urlsafe(32)
|
||||
|
||||
credential_create_options = webauthn.WebAuthnMakeCredentialOptions(
|
||||
challenge,
|
||||
"SimpleLogin",
|
||||
RP_ID,
|
||||
fido_uuid,
|
||||
current_user.email,
|
||||
current_user.name,
|
||||
False,
|
||||
attestation="none",
|
||||
)
|
||||
|
||||
# Don't think this one should be used, but it's not configurable by arguments
|
||||
# https://www.w3.org/TR/webauthn/#sctn-location-extension
|
||||
registration_dict = credential_create_options.registration_dict
|
||||
del registration_dict["extensions"]["webauthn.loc"]
|
||||
|
||||
session["fido_uuid"] = fido_uuid
|
||||
session["fido_challenge"] = challenge.rstrip("=")
|
||||
|
||||
return render_template(
|
||||
"dashboard/fido_setup.html",
|
||||
fido_token_form=fido_token_form,
|
||||
credential_create_options=registration_dict,
|
||||
)
|
|
@ -134,6 +134,17 @@ class User(db.Model, ModelMixin, UserMixin):
|
|||
db.Boolean, nullable=False, default=False, server_default="0"
|
||||
)
|
||||
|
||||
# Fields for WebAuthn
|
||||
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)
|
||||
|
||||
def fido_enabled(self) -> bool:
|
||||
if self.fido_uuid is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
# some users could have lifetime premium
|
||||
lifetime = db.Column(db.Boolean, default=False, nullable=False, server_default="0")
|
||||
|
||||
|
|
|
@ -37,4 +37,5 @@ flask_profiler
|
|||
facebook-sdk
|
||||
google-api-python-client
|
||||
google-auth-httplib2
|
||||
python-gnupg
|
||||
python-gnupg
|
||||
webauthn
|
|
@ -80,6 +80,7 @@ pycparser==2.19 # via cffi
|
|||
pycryptodome==3.9.4 # via -r requirements.in
|
||||
pygments==2.4.2 # via ipython
|
||||
pyopenssl==19.0.0 # via -r requirements.in
|
||||
webauthn==0.4.7 # via manually
|
||||
pyotp==2.3.0 # via -r requirements.in
|
||||
pyparsing==2.4.0 # via packaging
|
||||
pytest==4.6.3 # via -r requirements.in
|
||||
|
|
122
static/assets/js/vendors/base64.js
vendored
Normal file
122
static/assets/js/vendors/base64.js
vendored
Normal file
|
@ -0,0 +1,122 @@
|
|||
// Copyright (c) 2017 Duo Security, Inc. All rights reserved.
|
||||
// Under BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/duo-labs/py_webauthn/
|
||||
|
||||
var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
|
||||
;(function (exports) {
|
||||
'use strict'
|
||||
|
||||
var Arr = (typeof Uint8Array !== 'undefined')
|
||||
? Uint8Array
|
||||
: Array
|
||||
|
||||
var PLUS = '+'.charCodeAt(0)
|
||||
var SLASH = '/'.charCodeAt(0)
|
||||
var NUMBER = '0'.charCodeAt(0)
|
||||
var LOWER = 'a'.charCodeAt(0)
|
||||
var UPPER = 'A'.charCodeAt(0)
|
||||
var PLUS_URL_SAFE = '-'.charCodeAt(0)
|
||||
var SLASH_URL_SAFE = '_'.charCodeAt(0)
|
||||
|
||||
function decode (elt) {
|
||||
var code = elt.charCodeAt(0)
|
||||
if (code === PLUS || code === PLUS_URL_SAFE) return 62 // '+'
|
||||
if (code === SLASH || code === SLASH_URL_SAFE) return 63 // '/'
|
||||
if (code < NUMBER) return -1 // no match
|
||||
if (code < NUMBER + 10) return code - NUMBER + 26 + 26
|
||||
if (code < UPPER + 26) return code - UPPER
|
||||
if (code < LOWER + 26) return code - LOWER + 26
|
||||
}
|
||||
|
||||
function b64ToByteArray (b64) {
|
||||
var i, j, l, tmp, placeHolders, arr
|
||||
|
||||
if (b64.length % 4 > 0) {
|
||||
throw new Error('Invalid string. Length must be a multiple of 4')
|
||||
}
|
||||
|
||||
// the number of equal signs (place holders)
|
||||
// if there are two placeholders, than the two characters before it
|
||||
// represent one byte
|
||||
// if there is only one, then the three characters before it represent 2 bytes
|
||||
// this is just a cheap hack to not do indexOf twice
|
||||
var len = b64.length
|
||||
placeHolders = b64.charAt(len - 2) === '=' ? 2 : b64.charAt(len - 1) === '=' ? 1 : 0
|
||||
|
||||
// base64 is 4/3 + up to two characters of the original data
|
||||
arr = new Arr(b64.length * 3 / 4 - placeHolders)
|
||||
|
||||
// if there are placeholders, only get up to the last complete 4 chars
|
||||
l = placeHolders > 0 ? b64.length - 4 : b64.length
|
||||
|
||||
var L = 0
|
||||
|
||||
function push (v) {
|
||||
arr[L++] = v
|
||||
}
|
||||
|
||||
for (i = 0, j = 0; i < l; i += 4, j += 3) {
|
||||
tmp = (decode(b64.charAt(i)) << 18) | (decode(b64.charAt(i + 1)) << 12) | (decode(b64.charAt(i + 2)) << 6) | decode(b64.charAt(i + 3))
|
||||
push((tmp & 0xFF0000) >> 16)
|
||||
push((tmp & 0xFF00) >> 8)
|
||||
push(tmp & 0xFF)
|
||||
}
|
||||
|
||||
if (placeHolders === 2) {
|
||||
tmp = (decode(b64.charAt(i)) << 2) | (decode(b64.charAt(i + 1)) >> 4)
|
||||
push(tmp & 0xFF)
|
||||
} else if (placeHolders === 1) {
|
||||
tmp = (decode(b64.charAt(i)) << 10) | (decode(b64.charAt(i + 1)) << 4) | (decode(b64.charAt(i + 2)) >> 2)
|
||||
push((tmp >> 8) & 0xFF)
|
||||
push(tmp & 0xFF)
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
function uint8ToBase64 (uint8) {
|
||||
var i
|
||||
var extraBytes = uint8.length % 3 // if we have 1 byte left, pad 2 bytes
|
||||
var output = ''
|
||||
var temp, length
|
||||
|
||||
function encode (num) {
|
||||
return lookup.charAt(num)
|
||||
}
|
||||
|
||||
function tripletToBase64 (num) {
|
||||
return encode(num >> 18 & 0x3F) + encode(num >> 12 & 0x3F) + encode(num >> 6 & 0x3F) + encode(num & 0x3F)
|
||||
}
|
||||
|
||||
// go through the array every three bytes, we'll deal with trailing stuff later
|
||||
for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
|
||||
temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2])
|
||||
output += tripletToBase64(temp)
|
||||
}
|
||||
|
||||
// pad the end with zeros, but make sure to not forget the extra bytes
|
||||
switch (extraBytes) {
|
||||
case 1:
|
||||
temp = uint8[uint8.length - 1]
|
||||
output += encode(temp >> 2)
|
||||
output += encode((temp << 4) & 0x3F)
|
||||
output += '=='
|
||||
break
|
||||
case 2:
|
||||
temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1])
|
||||
output += encode(temp >> 10)
|
||||
output += encode((temp >> 4) & 0x3F)
|
||||
output += encode((temp << 2) & 0x3F)
|
||||
output += '='
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
exports.toByteArray = b64ToByteArray
|
||||
exports.fromByteArray = uint8ToBase64
|
||||
}(typeof exports === 'undefined' ? (this.base64js = {}) : exports))
|
131
static/assets/js/vendors/webauthn.js
vendored
Normal file
131
static/assets/js/vendors/webauthn.js
vendored
Normal file
|
@ -0,0 +1,131 @@
|
|||
// Copyright (c) 2017 Duo Security, Inc. All rights reserved.
|
||||
// Under BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/duo-labs/py_webauthn/
|
||||
|
||||
function b64enc(buf) {
|
||||
return base64js
|
||||
.fromByteArray(buf)
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
}
|
||||
|
||||
function b64RawEnc(buf) {
|
||||
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
function hexEncode(buf) {
|
||||
return Array.from(buf)
|
||||
.map(function (x) {
|
||||
return ("0" + x.toString(16)).substr(-2);
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
const transformCredentialRequestOptions = (
|
||||
credentialRequestOptionsFromServer
|
||||
) => {
|
||||
let { challenge, allowCredentials } = credentialRequestOptionsFromServer;
|
||||
|
||||
challenge = Uint8Array.from(
|
||||
atob(challenge.replace(/\_/g, "/").replace(/\-/g, "+")),
|
||||
(c) => c.charCodeAt(0)
|
||||
);
|
||||
|
||||
allowCredentials = allowCredentials.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 transformedCredentialRequestOptions = Object.assign(
|
||||
{},
|
||||
credentialRequestOptionsFromServer,
|
||||
{ challenge, allowCredentials }
|
||||
);
|
||||
|
||||
return transformedCredentialRequestOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transforms items in the credentialCreateOptions generated on the server
|
||||
* into byte arrays expected by the navigator.credentials.create() call
|
||||
* @param {Object} credentialCreateOptionsFromServer
|
||||
*/
|
||||
const transformCredentialCreateOptions = (
|
||||
credentialCreateOptionsFromServer
|
||||
) => {
|
||||
let { challenge, user } = credentialCreateOptionsFromServer;
|
||||
user.id = Uint8Array.from(
|
||||
atob(
|
||||
credentialCreateOptionsFromServer.user.id
|
||||
.replace(/\_/g, "/")
|
||||
.replace(/\-/g, "+")
|
||||
),
|
||||
(c) => c.charCodeAt(0)
|
||||
);
|
||||
|
||||
challenge = Uint8Array.from(
|
||||
atob(
|
||||
credentialCreateOptionsFromServer.challenge
|
||||
.replace(/\_/g, "/")
|
||||
.replace(/\-/g, "+")
|
||||
),
|
||||
(c) => c.charCodeAt(0)
|
||||
);
|
||||
|
||||
const transformedCredentialCreateOptions = Object.assign(
|
||||
{},
|
||||
credentialCreateOptionsFromServer,
|
||||
{ challenge, user }
|
||||
);
|
||||
|
||||
return transformedCredentialCreateOptions;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Transforms the binary data in the credential into base64 strings
|
||||
* for posting to the server.
|
||||
* @param {PublicKeyCredential} newAssertion
|
||||
*/
|
||||
const transformNewAssertionForServer = (newAssertion) => {
|
||||
const attObj = new Uint8Array(newAssertion.response.attestationObject);
|
||||
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
|
||||
const rawId = new Uint8Array(newAssertion.rawId);
|
||||
|
||||
const registrationClientExtensions = newAssertion.getClientExtensionResults();
|
||||
|
||||
return {
|
||||
id: newAssertion.id,
|
||||
rawId: b64enc(rawId),
|
||||
type: newAssertion.type,
|
||||
attObj: b64enc(attObj),
|
||||
clientData: b64enc(clientDataJSON),
|
||||
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Encodes the binary data in the assertion into strings for posting to the server.
|
||||
* @param {PublicKeyCredential} newAssertion
|
||||
*/
|
||||
const transformAssertionForServer = (newAssertion) => {
|
||||
const authData = new Uint8Array(newAssertion.response.authenticatorData);
|
||||
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
|
||||
const rawId = new Uint8Array(newAssertion.rawId);
|
||||
const sig = new Uint8Array(newAssertion.response.signature);
|
||||
const assertionClientExtensions = newAssertion.getClientExtensionResults();
|
||||
|
||||
return {
|
||||
id: newAssertion.id,
|
||||
rawId: b64enc(rawId),
|
||||
type: newAssertion.type,
|
||||
authData: b64RawEnc(authData),
|
||||
clientData: b64RawEnc(clientDataJSON),
|
||||
signature: hexEncode(sig),
|
||||
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
|
||||
};
|
||||
};
|
Loading…
Reference in a new issue