From 368f2396ce0ae7e2b09705bf73b559e0df12f000 Mon Sep 17 00:00:00 2001 From: tuxcoder Date: Mon, 25 Dec 2023 19:44:38 +0100 Subject: [PATCH] remove totp, cleanup, bugfixes --- README.md | 16 +-- browser_app/index.js | 107 ------------------ lenticular_cloud/auth_providers.py | 32 +----- lenticular_cloud/form/auth.py | 7 -- lenticular_cloud/form/frontend.py | 18 +-- lenticular_cloud/model.py | 18 --- .../template/frontend/base.html.j2 | 1 - .../template/frontend/passkey_list.html.j2 | 4 +- .../template/frontend/totp.html.j2 | 35 ------ .../template/frontend/totp_new.html.j2 | 20 ---- lenticular_cloud/views/auth.py | 4 + lenticular_cloud/views/frontend.py | 46 +------- lenticular_cloud/views/oauth2.py | 7 +- 13 files changed, 22 insertions(+), 293 deletions(-) delete mode 100644 lenticular_cloud/template/frontend/totp.html.j2 delete mode 100644 lenticular_cloud/template/frontend/totp_new.html.j2 diff --git a/README.md b/README.md index 74f8e45..cd4c6c2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Lenticular Cloud ================ -Simple user Manager in LDAP +Simple user Manager proudly made in ~~LDAP~~ SQL @@ -11,15 +11,12 @@ Features * frontend for hydra * Web Platform to mange users -* client certs -* ldap backend, can be used by other services +* fake ldap backend, can be used by other services Auth Methods: ------------- - * U2F (TODO) - * TOTP * Password - * WebAuth (TODO) + * Passkey @@ -34,13 +31,6 @@ Tested Services -Oauth2 Settings: ----------------- - -callback url: `${domain}/ - - - Development =========== diff --git a/browser_app/index.js b/browser_app/index.js index 375933c..5d168a9 100644 --- a/browser_app/index.js +++ b/browser_app/index.js @@ -112,39 +112,6 @@ window.auth_passkey = { }, } -window.totp = { - init_list: function(){ - }, - init_new: function() { - //create new TOTP secret, create qrcode and ask for token. - var form = $('form'); - var secret = randBase32(); - var input_secret = form.querySelector('#secret') - if(input_secret.value == '') { - input_secret.value = secret; - } - - form.querySelector('#name').onchange=window.totp.generate_qrcode; - form.querySelector('#name').onkeyup=window.totp.generate_qrcode; - window.totp.generate_qrcode(); - }, - generate_qrcode: function(){ - var form = $('form'); - var secret = form.querySelector('#secret').value; - var name = form.querySelector('#name').value; - var issuer = 'Lenticular%20Cloud'; - var svg_container = $('#svg-container') - var svg = new QRCode(`otpauth://totp/${issuer}:${name}?secret=${secret}&issuer=${issuer}`).svg(); - var svg_xml =new DOMParser().parseFromString(svg,'text/xml') - if(svg_container.childNodes.length > 0) { - svg_container.childNodes[0].replaceWith(svg_xml.childNodes[0]) - } else { - svg_container.appendChild(svg_xml.childNodes[0]); - } - // .innerHtml=svg; - } -} - window.password_change= { init: function(){ var form = $('form'); @@ -182,77 +149,3 @@ window.oauth2_token = { return false; } } - - - -window.client_cert = { - init_list: function() { - // do fancy cert stats stuff - }, - init_new: function() { - // create localy key or import public key - - var form = $('form#gen-key-form'); - - }, - generate_private_key: function() { - var form = $('form#gen-key-form'); - var key_size = form.querySelector('#key-size').value; - var valid_time = form.querySelector('input[name=valid_time]').value; - $('button#generate-key').style['display'] = 'none'; - pki.rsa.generateKeyPair({bits: key_size, workers: 2}, function(err, keypair) { - console.log(keypair); - - //returns the exported key to a hidden form - var form_sign_key = $('#gen-key-sign form'); - form_sign_key.querySelector('textarea[name=publickey]').value = pki.publicKeyToPem(keypair.publicKey); - form_sign_key.querySelector('input[name=valid_time]').value = valid_time; - - SimpleFormSubmit.submitForm(form_sign_key.action, form_sign_key) - .then(response => { - response.json().then( json_data => { - if (json_data.errors) { - var msg =''; - new Dialog('Password change Error', `Error Happend: ${msg}`).show() - } else { - // get certificate - var data = response.data; - var certs = [ - pki.certificateFromPem(data.cert), - pki.certificateFromPem(data.ca_cert) - ]; - var password = form.querySelector('#cert-password').value; - var p12Asn1; - if (password == '') { - p12Asn1 = pkcs12.toPkcs12Asn1(keypair.privateKey, certs, null, {algorithm: '3des'}); // without password - } else { - p12Asn1 = pkcs12.toPkcs12Asn1(keypair.privateKey, certs, password, {algorithm: '3des'}); // without password - } - var p12Der = asn1.toDer(p12Asn1).getBytes(); - var p12b64 = util.encode64(p12Der); - - - var button = $('#save-button'); - button.href= "data:application/x-pkcs12;base64," + p12b64 - button.style['display'] ='block'; - //new Dialog('Password changed', 'Password changed successfully!').show(); - } - }); - }); - }); - }, - revoke_certificate: function(href, id){ - var dialog = new ConfirmDialog('Revoke client certificate', `Are you sure to revoke the certificate with the fingerprint ${id}?`); - dialog.show().then(()=>{ - fetch(href, { - method: 'DELETE' - }); - }); - return false; - } -}; - diff --git a/lenticular_cloud/auth_providers.py b/lenticular_cloud/auth_providers.py index 24af7ec..6c2cda3 100644 --- a/lenticular_cloud/auth_providers.py +++ b/lenticular_cloud/auth_providers.py @@ -47,38 +47,8 @@ class PasswordAuthProvider(AuthProvider): return compare_hash(crypt.crypt(password, user.password_hashed),user.password_hashed) -class U2FAuthProvider(AuthProvider): - @staticmethod - def get_from() -> FlaskForm: - return Fido2Form(prefix='fido2') - - -class WebAuthProvider(AuthProvider): - pass - - -class TotpAuthProvider(AuthProvider): - - @staticmethod - def get_form(): - return TotpForm(prefix='totp') - - @staticmethod - def check_auth(user: User, form: FlaskForm) -> bool: - data = form.data['totp'] - if data is not None: - #print(f'data totp: {data}') - if len(user.totps) == 0: # migration, TODO remove - return True - for totp in user.totps: - if totp.verify(data): - return True - return False - - AUTH_PROVIDER_LIST = [ - PasswordAuthProvider, - TotpAuthProvider + PasswordAuthProvider ] #print(LdapAuthProvider.get_name()) diff --git a/lenticular_cloud/form/auth.py b/lenticular_cloud/form/auth.py index 7bc1c15..064e90c 100644 --- a/lenticular_cloud/form/auth.py +++ b/lenticular_cloud/form/auth.py @@ -20,13 +20,6 @@ class PasswordForm(FlaskForm): password = PasswordField(gettext('Password')) submit = SubmitField(gettext('Authorize')) - -class TotpForm(FlaskForm): - totp = StringField(gettext('2FA Token')) - submit = SubmitField(gettext('Authorize')) - - - class ConsentForm(FlaskForm): # scopes = SelectMultipleField(gettext('scopes')) # audiences = SelectMultipleField(gettext('audiences')) diff --git a/lenticular_cloud/form/frontend.py b/lenticular_cloud/form/frontend.py index c11e4a9..43b4daf 100644 --- a/lenticular_cloud/form/frontend.py +++ b/lenticular_cloud/form/frontend.py @@ -22,17 +22,6 @@ class ClientCertForm(FlaskForm): ]) submit = SubmitField(gettext('Submit')) - -class TOTPForm(FlaskForm): - secret = HiddenField(gettext('totp-Secret')) - token = StringField(gettext('totp-verify token')) - name = StringField(gettext('name')) - submit = SubmitField(gettext('Activate')) - - -class TOTPDeleteForm(FlaskForm): - submit = SubmitField(gettext('Delete')) - class AppTokenForm(FlaskForm): name = StringField(gettext('name'), validators=[DataRequired(),Length(min=1, max=255) ]) scopes = StringField(gettext('scopes'), validators=[DataRequired(),Length(min=1, max=255) ]) @@ -41,11 +30,10 @@ class AppTokenForm(FlaskForm): class AppTokenDeleteForm(FlaskForm): submit = SubmitField(gettext('Delete')) -class WebauthnRegisterForm(FlaskForm): - """webauthn register token form""" +class PasskeyRegisterForm(FlaskForm): + """Passkey register form""" - attestation = HiddenField('Attestation', [InputRequired()]) - name = StringField('Name', [Length(max=250)]) + name = StringField('Name', [Length(max=50)]) submit = SubmitField('Register', render_kw={'disabled': True}) class PasswordChangeForm(FlaskForm): diff --git a/lenticular_cloud/model.py b/lenticular_cloud/model.py index 799ce2d..27a1f82 100644 --- a/lenticular_cloud/model.py +++ b/lenticular_cloud/model.py @@ -163,12 +163,8 @@ class User(BaseModel, ModelUpdatedMixin): enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, default=False) app_tokens: Mapped[List['AppToken']] = relationship('AppToken', back_populates='user') - # totps: Mapped[List['Totp']] = relationship('Totp', back_populates='user', default_factory=list) passkey_credentials: Mapped[List['PasskeyCredential']] = relationship('PasskeyCredential', back_populates='user', cascade='delete,delete-orphan', passive_deletes=True) - @property - def totps(self) -> List['Totp']: - return [] def __init__(self, **kwargs) -> None: super().__init__(**kwargs) @@ -226,20 +222,6 @@ class AppToken(BaseModel, ModelUpdatedMixin): token = ''.join(secrets.choice(alphabet) for i in range(12)) return AppToken(scopes=scopes, token=token, user=user, name=name) -class Totp(BaseModel, ModelUpdatedMixin): - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - secret: Mapped[str] = mapped_column(db.String, nullable=False) - name: Mapped[str] = mapped_column(db.String, nullable=False) - - user_id: Mapped[uuid.UUID] = mapped_column( - db.Uuid, - db.ForeignKey(User.id), nullable=False) - # user: Mapped[User] = relationship(User, back_populates="totp") - last_used: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True, default=None) - - def verify(self, token: str) -> bool: - totp = pyotp.TOTP(self.secret) - return totp.verify(token) class PasskeyCredential(BaseModel, ModelUpdatedMixin): # pylint: disable=too-few-public-methods diff --git a/lenticular_cloud/template/frontend/base.html.j2 b/lenticular_cloud/template/frontend/base.html.j2 index 42ff924..eef0908 100644 --- a/lenticular_cloud/template/frontend/base.html.j2 +++ b/lenticular_cloud/template/frontend/base.html.j2 @@ -20,7 +20,6 @@ {##} - diff --git a/lenticular_cloud/template/frontend/passkey_list.html.j2 b/lenticular_cloud/template/frontend/passkey_list.html.j2 index f8bf437..40e0756 100644 --- a/lenticular_cloud/template/frontend/passkey_list.html.j2 +++ b/lenticular_cloud/template/frontend/passkey_list.html.j2 @@ -21,10 +21,12 @@ {{ credential.credential_id[0:8].hex() }}... {{ credential.last_used }} {{ credential.created_at }}... - {{ render_form(button_form, action_url=url_for('.passkey_delete', id=credential.id), action_text='delete', btn_class='btn btn-danger') }} + + {{ render_form(button_form, action_url=url_for('.passkey_delete', id=credential.id), action_text='delete', btn_class='btn btn-danger') }} {% endfor %} + Add new Passkey {% endblock %} diff --git a/lenticular_cloud/template/frontend/totp.html.j2 b/lenticular_cloud/template/frontend/totp.html.j2 deleted file mode 100644 index 18f5d21..0000000 --- a/lenticular_cloud/template/frontend/totp.html.j2 +++ /dev/null @@ -1,35 +0,0 @@ -{% extends 'frontend/base.html.j2' %} - -{% block title %}{{ gettext('2FA - TOTP') }}{% endblock %} - -{% block content %} - - - - - - - - - - {% for totp in current_user.totps %} - - - - - {% endfor %} -
namecreated_ataction -
{{ totp.name }}{{ totp.created_at }}{{ render_form(delete_form, action_url=url_for('frontend.totp_delete', totp_name=totp.name)) }}
- - New TOTP - - -{% endblock %} - - -{% block script_js %} - -totp.init_list(); - - -{% endblock %} diff --git a/lenticular_cloud/template/frontend/totp_new.html.j2 b/lenticular_cloud/template/frontend/totp_new.html.j2 deleted file mode 100644 index 6853a57..0000000 --- a/lenticular_cloud/template/frontend/totp_new.html.j2 +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'frontend/base.html.j2' %} - -{% block title %}{{ gettext('2FA - TOTP - New') }}{% endblock %} - -{% block content %} - -{{ render_form(form) }} - -
-
- -{% endblock %} - - -{% block script_js %} - -totp.init_new(); - - -{% endblock %} diff --git a/lenticular_cloud/views/auth.py b/lenticular_cloud/views/auth.py index 62cca3b..80e3973 100644 --- a/lenticular_cloud/views/auth.py +++ b/lenticular_cloud/views/auth.py @@ -51,9 +51,13 @@ async def consent() -> ResponseReturnValue: if form.validate_on_submit() or consent_request.skip: + if type(consent_request.subject) != str: + logger.error("not set subject `consent_request.subject`") + return 'internal error', 500 uid = UUID(consent_request.subject) user = User.query.get(uid) if user is None: + logger.error("user not found, even if it should exist") return 'internal error', 500 access_token = { 'name': str(user.username), diff --git a/lenticular_cloud/views/frontend.py b/lenticular_cloud/views/frontend.py index 9337eff..3f1493a 100644 --- a/lenticular_cloud/views/frontend.py +++ b/lenticular_cloud/views/frontend.py @@ -25,10 +25,9 @@ from webauthn.helpers.structs import ( UserVerificationRequirement, ) -from ..model import db, User, Totp, AppToken, PasskeyCredential -from ..form.frontend import ClientCertForm, TOTPForm, \ - TOTPDeleteForm, PasswordChangeForm, WebauthnRegisterForm, \ - AppTokenForm, AppTokenDeleteForm +from ..model import db, User, AppToken, PasskeyCredential +from ..form.frontend import ClientCertForm, PasswordChangeForm, \ + AppTokenForm, AppTokenDeleteForm, PasskeyRegisterForm from ..form.base import ButtonForm from ..auth_providers import PasswordAuthProvider from .oauth2 import redirect_login, oauth2 @@ -188,43 +187,6 @@ def app_token_delete(app_token_name: str) -> ResponseReturnValue: return redirect(url_for('frontend.app_token')) -@frontend_views.route('/totp') -def totp() -> ResponseReturnValue: - delete_form = TOTPDeleteForm() - return render_template('frontend/totp.html.j2', delete_form=delete_form) - - -@frontend_views.route('/totp/new', methods=['GET', 'POST']) -def totp_new() -> ResponseReturnValue: - form = TOTPForm() - - if form.validate_on_submit(): - totp = Totp(name=form.data['name'], secret=form.data['secret'], user=get_current_user()) - if totp.verify(form.data['token']): - get_current_user().totps.append(totp) - db.session.commit() - return jsonify({ - 'status': 'ok'}) - else: - return jsonify({ - 'status': 'error', - 'errors': [ - 'TOTP Token invalid' - ]}) - return render_template('frontend/totp_new.html.j2', form=form) - - -@frontend_views.route('/totp//delete', methods=['GET', 'POST']) -def totp_delete(totp_name) -> ResponseReturnValue: - totp = Totp.query.filter(Totp.name == totp_name).first() # type: Optional[Totp] - db.session.delete(totp) - db.session.commit() - - return jsonify({ - 'status': 'ok'}) - - - ## Passkey @frontend_views.route('/passkey/list', methods=['GET']) @@ -243,7 +205,7 @@ def passkey_new() -> ResponseReturnValue: user = get_current_user() # type: User - form = WebauthnRegisterForm() + form = PasskeyRegisterForm() options = webauthn.generate_registration_options( rp_name="Lenticluar Cloud", diff --git a/lenticular_cloud/views/oauth2.py b/lenticular_cloud/views/oauth2.py index 3767731..26e290e 100644 --- a/lenticular_cloud/views/oauth2.py +++ b/lenticular_cloud/views/oauth2.py @@ -1,7 +1,7 @@ from authlib.integrations.flask_client import OAuth from authlib.integrations.base_client.errors import MismatchingStateError, OAuthError -from flask import Flask, Blueprint, Response, session, request, redirect, url_for -from flask_login import login_user, logout_user, current_user +from flask import Flask, Blueprint, current_app, session, request, redirect, url_for +from flask_login import login_user, logout_user from flask.typing import ResponseReturnValue from flask_login import LoginManager from typing import Optional @@ -29,7 +29,8 @@ login_manager = LoginManager() def redirect_login() -> ResponseReturnValue: logout_user() session['next_url'] = request.path - redirect_uri = url_for('oauth2.authorized', _external=True) + public_url = current_app.config['PUBLIC_URL'] + redirect_uri = public_url + url_for('oauth2.authorized') response = oauth2.custom.authorize_redirect(redirect_uri) if not isinstance(response, WerkzeugResponse): raise RuntimeError("invalid redirect")