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 ='
';
- for( var field in json_data.repsonse) {
- msg += `- ${field}: ${data.errors[field]}
`;
- }
- 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 @@
{#{{ gettext('Client Cert') }}#}
{{ gettext('Passkey') }}
{{ gettext('App Tokens') }}
- {{ gettext('2FA - TOTP') }}
{{ gettext('Oauth2 Tokens') }}
{{ gettext('Password Change') }}
{{ gettext('Logout') }}
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 %}
-
-
-
-
- name |
- created_at |
- action |
- |
-
-
- {% for totp in current_user.totps %}
-
- {{ totp.name }} |
- {{ totp.created_at }} |
- {{ render_form(delete_form, action_url=url_for('frontend.totp_delete', totp_name=totp.name)) }} |
- {% endfor %}
-
-
- 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")