add partial fido2/WebAuthn
This commit is contained in:
parent
6c8bb99c61
commit
5401e2594d
|
@ -121,6 +121,7 @@ in
|
||||||
blinker
|
blinker
|
||||||
ory-hydra-client
|
ory-hydra-client
|
||||||
authlib # as oauth client lib
|
authlib # as oauth client lib
|
||||||
|
fido2 # for webauthn
|
||||||
flask_migrate # db migrations
|
flask_migrate # db migrations
|
||||||
|
|
||||||
gunicorn
|
gunicorn
|
||||||
|
@ -142,5 +143,6 @@ in
|
||||||
|
|
||||||
nose
|
nose
|
||||||
mypy
|
mypy
|
||||||
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ def create_app() -> Flask:
|
||||||
|
|
||||||
#app.ldap_orm = Connection(app.config['LDAP_URL'], app.config['LDAP_BIND_DN'], app.config['LDAP_BIND_PW'], auto_bind=True)
|
#app.ldap_orm = Connection(app.config['LDAP_URL'], app.config['LDAP_BIND_DN'], app.config['LDAP_BIND_PW'], auto_bind=True)
|
||||||
server = Server(app.config['LDAP_URL'], get_info=ALL)
|
server = Server(app.config['LDAP_URL'], get_info=ALL)
|
||||||
app.ldap_conn = Connection(server, app.config['LDAP_BIND_DN'], app.config['LDAP_BIND_PW'], auto_bind=True)
|
app.ldap_conn = Connection(server, app.config['LDAP_BIND_DN'], app.config['LDAP_BIND_PW'], auto_bind="DEFAULT")
|
||||||
model.ldap_conn = app.ldap_conn
|
model.ldap_conn = app.ldap_conn
|
||||||
model.base_dn = app.config['LDAP_BASE_DN']
|
model.base_dn = app.config['LDAP_BASE_DN']
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import argparse
|
||||||
from .model import db, User, UserSignUp
|
from .model import db, User, UserSignUp
|
||||||
from .app import create_app
|
from .app import create_app
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
from flask_migrate import upgrade
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
@ -23,6 +24,9 @@ def entry_point():
|
||||||
parser_run = subparsers.add_parser('run')
|
parser_run = subparsers.add_parser('run')
|
||||||
parser_run.set_defaults(func=cli_run)
|
parser_run.set_defaults(func=cli_run)
|
||||||
|
|
||||||
|
parser_db_upgrade = subparsers.add_parser('db_upgrade')
|
||||||
|
parser_db_upgrade.set_defaults(func=cli_db_upgrade)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
parser_upcoming = subparsers.add_parser('upcoming')
|
parser_upcoming = subparsers.add_parser('upcoming')
|
||||||
parser_upcoming.set_defaults(func=cli_upcoming)
|
parser_upcoming.set_defaults(func=cli_upcoming)
|
||||||
|
@ -73,6 +77,10 @@ def cli_run(app, args):
|
||||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
|
||||||
app.run(debug=True, host='127.0.0.1', port=5000)
|
app.run(debug=True, host='127.0.0.1', port=5000)
|
||||||
|
|
||||||
|
def cli_db_upgrade(args):
|
||||||
|
upgrade()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
entry_point()
|
entry_point()
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,11 @@ from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, SubmitField, TextField, \
|
from wtforms import StringField, SubmitField, TextField, \
|
||||||
TextAreaField, PasswordField, IntegerField, FloatField, \
|
TextAreaField, PasswordField, IntegerField, FloatField, \
|
||||||
DateTimeField, DateField, FormField, BooleanField, \
|
DateTimeField, DateField, FormField, BooleanField, \
|
||||||
SelectField, Form as NoCsrfForm, SelectMultipleField
|
SelectField, Form as NoCsrfForm, SelectMultipleField, \
|
||||||
|
HiddenField
|
||||||
from wtforms.fields.html5 import EmailField
|
from wtforms.fields.html5 import EmailField
|
||||||
from wtforms.widgets.html5 import NumberInput, DateInput
|
from wtforms.widgets.html5 import NumberInput, DateInput
|
||||||
from wtforms.validators import DataRequired, NumberRange, Optional, NoneOf, Length, Regexp
|
from wtforms.validators import DataRequired, NumberRange, Optional, NoneOf, Length, Regexp, InputRequired
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,6 +26,11 @@ class TotpForm(FlaskForm):
|
||||||
submit = SubmitField(gettext('Authorize'))
|
submit = SubmitField(gettext('Authorize'))
|
||||||
|
|
||||||
|
|
||||||
|
class WebauthnLoginForm(FlaskForm):
|
||||||
|
"""webauthn login form"""
|
||||||
|
|
||||||
|
assertion = HiddenField('Assertion', [InputRequired()])
|
||||||
|
|
||||||
class Fido2Form(FlaskForm):
|
class Fido2Form(FlaskForm):
|
||||||
fido2 = TextField(gettext('Fido2'), default="Javascript Required")
|
fido2 = TextField(gettext('Fido2'), default="Javascript Required")
|
||||||
submit = SubmitField(gettext('Authorize'))
|
submit = SubmitField(gettext('Authorize'))
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import SelectField, FieldList as WTFFieldList, Form
|
from wtforms import SelectField, FieldList as WTFFieldList, Form
|
||||||
from wtforms.fields import Field
|
from wtforms.fields import Field
|
||||||
from ..model import db
|
from ..model import db
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ButtonForm(FlaskForm):
|
||||||
|
"""only button form"""
|
||||||
|
|
||||||
|
|
||||||
class FieldList(WTFFieldList):
|
class FieldList(WTFFieldList):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.modify = kwargs.pop("modify", True)
|
self.modify = kwargs.pop("modify", True)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from wtforms import StringField, SubmitField, TextField, \
|
||||||
SelectField, Form as NoCsrfForm, HiddenField
|
SelectField, Form as NoCsrfForm, HiddenField
|
||||||
from wtforms.widgets.html5 import NumberInput, DateInput
|
from wtforms.widgets.html5 import NumberInput, DateInput
|
||||||
from wtforms.validators import DataRequired, NumberRange, \
|
from wtforms.validators import DataRequired, NumberRange, \
|
||||||
Optional, NoneOf, Length, EqualTo
|
Optional, NoneOf, Length, EqualTo, InputRequired
|
||||||
|
|
||||||
|
|
||||||
class ClientCertForm(FlaskForm):
|
class ClientCertForm(FlaskForm):
|
||||||
|
@ -34,6 +34,13 @@ class TOTPDeleteForm(FlaskForm):
|
||||||
submit = SubmitField(gettext('Delete'))
|
submit = SubmitField(gettext('Delete'))
|
||||||
|
|
||||||
|
|
||||||
|
class WebauthnRegisterForm(FlaskForm):
|
||||||
|
"""webauthn register token form"""
|
||||||
|
|
||||||
|
attestation = HiddenField('Attestation', [InputRequired()])
|
||||||
|
name = StringField('Name', [Length(max=250)])
|
||||||
|
submit = SubmitField('Register', render_kw={'disabled': True})
|
||||||
|
|
||||||
class PasswordChangeForm(FlaskForm):
|
class PasswordChangeForm(FlaskForm):
|
||||||
password_old = PasswordField(gettext('Old Password'), validators=[DataRequired()])
|
password_old = PasswordField(gettext('Old Password'), validators=[DataRequired()])
|
||||||
password_new = PasswordField(gettext('New Password'), validators=[DataRequired()])
|
password_new = PasswordField(gettext('New Password'), validators=[DataRequired()])
|
||||||
|
|
|
@ -239,6 +239,7 @@ class User(EntryBase):
|
||||||
last_login = db.Column(db.DateTime, nullable=True)
|
last_login = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
totps = db.relationship('Totp', back_populates='user')
|
totps = db.relationship('Totp', back_populates='user')
|
||||||
|
webauthn_credentials = db.relationship('WebauthnCredential', back_populates='user', cascade='delete,delete-orphan', passive_deletes=True)
|
||||||
|
|
||||||
dn = "uid={uid},{base_dn}"
|
dn = "uid={uid},{base_dn}"
|
||||||
base_dn = "ou=users,{_base_dn}"
|
base_dn = "ou=users,{_base_dn}"
|
||||||
|
@ -297,7 +298,7 @@ class User(EntryBase):
|
||||||
|
|
||||||
def by_username(self, username) -> Optional['User']:
|
def by_username(self, username) -> Optional['User']:
|
||||||
result = self._query('(uid={username:s})'.format(username=escape_filter_chars(username)))
|
result = self._query('(uid={username:s})'.format(username=escape_filter_chars(username)))
|
||||||
if len(result) > 0:
|
if len(result) > 0 and isinstance(result[0], User):
|
||||||
return result[0]
|
return result[0]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -334,6 +335,20 @@ class Totp(db.Model):
|
||||||
totp = pyotp.TOTP(self.secret)
|
totp = pyotp.TOTP(self.secret)
|
||||||
return totp.verify(token)
|
return totp.verify(token)
|
||||||
|
|
||||||
|
|
||||||
|
class WebauthnCredential(db.Model): # pylint: disable=too-few-public-methods
|
||||||
|
"""Webauthn credential model"""
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
|
||||||
|
user_handle = db.Column(db.String(64), nullable=False)
|
||||||
|
credential_data = db.Column(db.LargeBinary, nullable=False)
|
||||||
|
name = db.Column(db.String(250))
|
||||||
|
registered = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
user = db.relationship('User', back_populates='webauthn_credentials')
|
||||||
|
|
||||||
|
|
||||||
class Group(EntryBase):
|
class Group(EntryBase):
|
||||||
__abstract__ = True # for sqlalchemy, disable for now
|
__abstract__ = True # for sqlalchemy, disable for now
|
||||||
dn = "cn={cn},{base_dn}"
|
dn = "cn={cn},{base_dn}"
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="{{url_for('.client_new')}}">Add Client</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
|
|
||||||
from authlib.integrations.flask_client import OAuth
|
|
||||||
from urllib.parse import urlencode, parse_qs
|
from urllib.parse import urlencode, parse_qs
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
|
@ -26,12 +25,13 @@ from ..model import db, User, SecurityUser, UserSignUp
|
||||||
from ..form.auth import ConsentForm, LoginForm, RegistrationForm
|
from ..form.auth import ConsentForm, LoginForm, RegistrationForm
|
||||||
from ..auth_providers import AUTH_PROVIDER_LIST
|
from ..auth_providers import AUTH_PROVIDER_LIST
|
||||||
from ..hydra import hydra_service
|
from ..hydra import hydra_service
|
||||||
|
from ..wrapped_fido2_server import WrappedFido2Server
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
auth_views = Blueprint('auth', __name__, url_prefix='/auth')
|
auth_views = Blueprint('auth', __name__, url_prefix='/auth')
|
||||||
|
webauthn = WrappedFido2Server()
|
||||||
|
|
||||||
|
|
||||||
@auth_views.route('/consent', methods=['GET', 'POST'])
|
@auth_views.route('/consent', methods=['GET', 'POST'])
|
||||||
|
@ -171,6 +171,21 @@ async def login_auth() -> ResponseReturnValue:
|
||||||
return render_template('auth/login_auth.html.j2', forms=auth_forms)
|
return render_template('auth/login_auth.html.j2', forms=auth_forms)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@auth_views.route('/webauthn/pkcro', methods=['POST'])
|
||||||
|
def webauthn_pkcro_route():
|
||||||
|
"""login webauthn pkcro route"""
|
||||||
|
|
||||||
|
user = User.query.filter(User.id == session.get('webauthn_login_user_id')).one_or_none()
|
||||||
|
form = ButtonForm()
|
||||||
|
if user and form.validate_on_submit():
|
||||||
|
pkcro, state = webauthn.authenticate_begin(webauthn_credentials(user))
|
||||||
|
session['webauthn_login_state'] = state
|
||||||
|
return Response(b64encode(cbor.encode(pkcro)).decode('utf-8'), mimetype='text/plain')
|
||||||
|
|
||||||
|
return '', HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
|
||||||
@auth_views.route("/logout")
|
@auth_views.route("/logout")
|
||||||
async def logout() -> ResponseReturnValue:
|
async def logout() -> ResponseReturnValue:
|
||||||
logout_challenge = request.args.get('logout_challenge')
|
logout_challenge = request.args.get('logout_challenge')
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
|
|
||||||
from authlib.integrations.base_client.errors import MissingTokenError, InvalidTokenError
|
from authlib.integrations.base_client.errors import MissingTokenError, InvalidTokenError
|
||||||
from urllib.parse import urlencode, parse_qs
|
from base64 import b64encode, b64decode
|
||||||
from flask import Blueprint, redirect, request
|
from fido2 import cbor
|
||||||
|
from fido2.client import ClientData
|
||||||
|
from fido2.ctap2 import AttestationObject, AttestedCredentialData, AuthenticatorData
|
||||||
|
from flask import Blueprint, Response, redirect, request
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask import jsonify, session
|
from flask import jsonify, session, flash
|
||||||
from flask import render_template, url_for
|
from flask import render_template, url_for
|
||||||
from flask_login import login_user, logout_user, current_user
|
from flask_login import login_user, logout_user, current_user
|
||||||
|
from http import HTTPStatus
|
||||||
from werkzeug.utils import redirect
|
from werkzeug.utils import redirect
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
@ -14,12 +18,17 @@ from flask.typing import ResponseReturnValue
|
||||||
from oauthlib.oauth2.rfc6749.errors import TokenExpiredError
|
from oauthlib.oauth2.rfc6749.errors import TokenExpiredError
|
||||||
from ory_hydra_client.api.admin import list_subject_consent_sessions, revoke_consent_sessions
|
from ory_hydra_client.api.admin import list_subject_consent_sessions, revoke_consent_sessions
|
||||||
from ory_hydra_client.models import GenericError
|
from ory_hydra_client.models import GenericError
|
||||||
|
from urllib.parse import urlencode, parse_qs
|
||||||
|
from random import SystemRandom
|
||||||
|
import string
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ..model import db, User, SecurityUser, Totp
|
from ..model import db, User, SecurityUser, Totp, WebauthnCredential
|
||||||
from ..form.frontend import ClientCertForm, TOTPForm, \
|
from ..form.frontend import ClientCertForm, TOTPForm, \
|
||||||
TOTPDeleteForm, PasswordChangeForm
|
TOTPDeleteForm, PasswordChangeForm, WebauthnRegisterForm
|
||||||
|
from ..form.base import ButtonForm
|
||||||
from ..auth_providers import LdapAuthProvider
|
from ..auth_providers import LdapAuthProvider
|
||||||
|
from .auth import webauthn
|
||||||
from .oauth2 import redirect_login, oauth2
|
from .oauth2 import redirect_login, oauth2
|
||||||
from ..hydra import hydra_service
|
from ..hydra import hydra_service
|
||||||
|
|
||||||
|
@ -161,6 +170,87 @@ def totp_delete(totp_name) -> ResponseReturnValue:
|
||||||
'status': 'ok'})
|
'status': 'ok'})
|
||||||
|
|
||||||
|
|
||||||
|
@frontend_views.route('/webauthn/list', methods=['GET'])
|
||||||
|
def webauthn_list_route() -> ResponseReturnValue:
|
||||||
|
"""list registered credentials for current user"""
|
||||||
|
|
||||||
|
creds = WebauthnCredential.query.all()
|
||||||
|
return render_template('webauthn_list.html', creds=creds, button_form=ButtonForm())
|
||||||
|
|
||||||
|
|
||||||
|
@frontend_views.route('/webauthn/delete/<webauthn_id>', methods=['POST'])
|
||||||
|
def webauthn_delete_route(webauthn_id: str) -> ResponseReturnValue:
|
||||||
|
"""delete registered credential"""
|
||||||
|
|
||||||
|
form = ButtonForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
cred = WebauthnCredential.query.filter(WebauthnCredential.id == webauthn_id).one()
|
||||||
|
db.session.delete(cred)
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for('app.webauthn_list_route'))
|
||||||
|
|
||||||
|
return '', HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def webauthn_credentials(user: User) -> list[AttestedCredentialData]:
|
||||||
|
"""get and decode all credentials for given user"""
|
||||||
|
return [AttestedCredentialData.create(**cbor.decode(cred.credential_data)) for cred in user.webauthn_credentials]
|
||||||
|
|
||||||
|
|
||||||
|
def random_string(length=32) -> str:
|
||||||
|
"""generates random string"""
|
||||||
|
return ''.join([SystemRandom().choice(string.ascii_letters + string.digits) for i in range(length)])
|
||||||
|
|
||||||
|
|
||||||
|
@frontend_views.route('/webauthn/pkcco', methods=['POST'])
|
||||||
|
def webauthn_pkcco_route() -> ResponseReturnValue:
|
||||||
|
"""get publicKeyCredentialCreationOptions"""
|
||||||
|
|
||||||
|
form = ButtonForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user = User.query.get(current_user.id)
|
||||||
|
user_handle = random_string()
|
||||||
|
exclude_credentials = webauthn_credentials(user)
|
||||||
|
pkcco, state = webauthn.register_begin(
|
||||||
|
{'id': user_handle.encode('utf-8'), 'name': user.username, 'displayName': user.username},
|
||||||
|
exclude_credentials)
|
||||||
|
session['webauthn_register_user_handle'] = user_handle
|
||||||
|
session['webauthn_register_state'] = state
|
||||||
|
return Response(b64encode(cbor.encode(pkcco)).decode('utf-8'), mimetype='text/plain')
|
||||||
|
|
||||||
|
return '', HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
|
||||||
|
@frontend_views.route('/webauthn/register', methods=['GET', 'POST'])
|
||||||
|
def webauthn_register_route() -> ResponseReturnValue:
|
||||||
|
"""register credential for current user"""
|
||||||
|
|
||||||
|
user = User.query.get(current_user.id)
|
||||||
|
form = WebauthnRegisterForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
try:
|
||||||
|
attestation = cbor.decode(b64decode(form.attestation.data))
|
||||||
|
auth_data = webauthn.register_complete(
|
||||||
|
session.pop('webauthn_register_state'),
|
||||||
|
ClientData(attestation['clientDataJSON']),
|
||||||
|
AttestationObject(attestation['attestationObject']))
|
||||||
|
|
||||||
|
db.session.add(WebauthnCredential(
|
||||||
|
user_id=user.id,
|
||||||
|
user_handle=session.pop('webauthn_register_user_handle'),
|
||||||
|
credential_data=cbor.encode(auth_data.credential_data.__dict__),
|
||||||
|
name=form.name.data))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return redirect(url_for('app.webauthn_list_route'))
|
||||||
|
except (KeyError, ValueError) as e:
|
||||||
|
current_app.logger.exception(e)
|
||||||
|
flash('Error during registration.', 'error')
|
||||||
|
|
||||||
|
return render_template('webauthn_register.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
@frontend_views.route('/password_change')
|
@frontend_views.route('/password_change')
|
||||||
def password_change() -> ResponseReturnValue:
|
def password_change() -> ResponseReturnValue:
|
||||||
form = PasswordChangeForm()
|
form = PasswordChangeForm()
|
||||||
|
@ -186,10 +276,10 @@ def password_change_post() -> ResponseReturnValue:
|
||||||
|
|
||||||
|
|
||||||
@frontend_views.route('/oauth2_token')
|
@frontend_views.route('/oauth2_token')
|
||||||
def oauth2_tokens() -> ResponseReturnValue:
|
async def oauth2_tokens() -> ResponseReturnValue:
|
||||||
|
|
||||||
subject = oauth2.custom.get('/userinfo').json()['sub']
|
subject = oauth2.custom.get('/userinfo').json()['sub']
|
||||||
consent_sessions = list_subject_consent_sessions.sync(subject=subject, _client=hydra_service.hydra_client)
|
consent_sessions = await list_subject_consent_sessions.asyncio(subject=subject, _client=hydra_service.hydra_client)
|
||||||
if consent_sessions is None or isinstance( consent_sessions, GenericError):
|
if consent_sessions is None or isinstance( consent_sessions, GenericError):
|
||||||
return 'internal error, could not fetch sessions', 500
|
return 'internal error, could not fetch sessions', 500
|
||||||
return render_template(
|
return render_template(
|
||||||
|
@ -198,9 +288,9 @@ def oauth2_tokens() -> ResponseReturnValue:
|
||||||
|
|
||||||
|
|
||||||
@frontend_views.route('/oauth2_token/<client_id>', methods=['DELETE'])
|
@frontend_views.route('/oauth2_token/<client_id>', methods=['DELETE'])
|
||||||
def oauth2_token_revoke(client_id: str) -> ResponseReturnValue:
|
async def oauth2_token_revoke(client_id: str) -> ResponseReturnValue:
|
||||||
subject = oauth2.session.get('/userinfo').json()['sub']
|
subject = oauth2.session.get('/userinfo').json()['sub']
|
||||||
revoke_consent_sessions.sync( _client=hydra_service.hydra_client,
|
await revoke_consent_sessions.asyncio( _client=hydra_service.hydra_client,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
client=client_id)
|
client=client_id)
|
||||||
|
|
||||||
|
|
21
lenticular_cloud/wrapped_fido2_server.py
Normal file
21
lenticular_cloud/wrapped_fido2_server.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# This file is part of sner4 project governed by MIT license, see the LICENSE.txt file.
|
||||||
|
# source: https://github.com/bodik/flask-webauthn-example/blob/master/fwe/wrapped_fido2_server.py
|
||||||
|
"""
|
||||||
|
yubico fido2 server wrapped for flask factory pattern delayed configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
from socket import getfqdn
|
||||||
|
|
||||||
|
from fido2.server import Fido2Server, PublicKeyCredentialRpEntity
|
||||||
|
|
||||||
|
|
||||||
|
class WrappedFido2Server(Fido2Server):
|
||||||
|
"""yubico fido2 server wrapped for flask factory pattern delayed configuration"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""initialize with default rp name"""
|
||||||
|
super().__init__(PublicKeyCredentialRpEntity(getfqdn(), 'name'))
|
||||||
|
|
||||||
|
def init_app(self, app) -> None:
|
||||||
|
"""reinitialize on factory pattern config request"""
|
||||||
|
super().__init__(PublicKeyCredentialRpEntity(app.config['SERVER_NAME'] or getfqdn(), 'name'))
|
37
migrations/versions/52a21983d2a8_add_webauthn.py
Normal file
37
migrations/versions/52a21983d2a8_add_webauthn.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
"""add webauthn
|
||||||
|
|
||||||
|
Revision ID: 52a21983d2a8
|
||||||
|
Revises: ff2f2e871dfc
|
||||||
|
Create Date: 2022-02-20 17:00:04.531393
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '52a21983d2a8'
|
||||||
|
down_revision = 'ff2f2e871dfc'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('webauthn_credential',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_handle', sa.String(length=64), nullable=False),
|
||||||
|
sa.Column('credential_data', sa.LargeBinary(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=250), nullable=True),
|
||||||
|
sa.Column('registered', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('webauthn_credential')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -7,6 +7,9 @@ Create Date: 2022-02-20 16:56:13.258209
|
||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import engine_from_config
|
||||||
|
from sqlalchemy.engine import reflection
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
|
@ -18,39 +21,54 @@ depends_on = None
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('user',
|
|
||||||
sa.Column('id', sa.String(length=36), nullable=False),
|
# init sate, migrate from non versioned db schema
|
||||||
sa.Column('username', sa.String(), nullable=False),
|
# by checking if tables exist
|
||||||
sa.Column('alternative_email', sa.String(), nullable=True),
|
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
config = op.get_context().config
|
||||||
sa.Column('modified_at', sa.DateTime(), nullable=False),
|
engine = engine_from_config(
|
||||||
sa.Column('last_login', sa.DateTime(), nullable=True),
|
config.get_section(config.config_ini_section), prefix="sqlalchemy."
|
||||||
sa.PrimaryKeyConstraint('id'),
|
|
||||||
sa.UniqueConstraint('username')
|
|
||||||
)
|
|
||||||
op.create_table('user_sign_up',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('username', sa.String(), nullable=False),
|
|
||||||
sa.Column('password', sa.String(), nullable=False),
|
|
||||||
sa.Column('alternative_email', sa.String(), nullable=True),
|
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_table('totp',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('secret', sa.String(), nullable=False),
|
|
||||||
sa.Column('name', sa.String(), nullable=False),
|
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.Column('user_id', sa.String(length=36), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
)
|
||||||
|
inspector = reflection.Inspector.from_engine(engine)
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
|
||||||
|
if 'user' not in tables:
|
||||||
|
op.create_table('user',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('username', sa.String(), nullable=False),
|
||||||
|
sa.Column('alternative_email', sa.String(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modified_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('last_login', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('username')
|
||||||
|
)
|
||||||
|
if 'user_sign_up' not in tables:
|
||||||
|
op.create_table('user_sign_up',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('username', sa.String(), nullable=False),
|
||||||
|
sa.Column('password', sa.String(), nullable=False),
|
||||||
|
sa.Column('alternative_email', sa.String(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
if 'totp' not in tables:
|
||||||
|
op.create_table('totp',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('secret', sa.String(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
|
pass
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_table('totp')
|
#op.drop_table('totp')
|
||||||
op.drop_table('user_sign_up')
|
#op.drop_table('user_sign_up')
|
||||||
op.drop_table('user')
|
#op.drop_table('user')
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
flask-debug
|
flask-debug
|
||||||
types-python-dateutil
|
types-python-dateutil
|
||||||
|
types-ldap3
|
||||||
|
|
Loading…
Reference in a new issue