merge ldap user and db user, cleanup, add password change

This commit is contained in:
TuxCoder 2020-05-27 17:56:10 +02:00
parent 6334c993a9
commit 4150853588
14 changed files with 280 additions and 259 deletions

View file

@ -3,10 +3,29 @@
const $ = document.querySelector.bind(document) const $ = document.querySelector.bind(document)
const _ = document.getElementById const _ = document.getElementById
export class ConfirmDialog { export class Dialog {
template(){
return `
<div class="modal-dialog">
<div class="modal-content">
constructor(message) { <div class="modal-header">
this._div = document.getElementById('confirm-dialog-template').content.querySelector('div').cloneNode(true); <h5 class="modal-title"></h5>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary close" data-dismiss="modal">Close</button>
</div>
</div>
</div>`;
}
constructor(title, message) {
this._div = (new DOMParser().parseFromString(this.template(), 'text/html')).body.firstChild;
this._div.querySelector('.modal-body').innerHTML = message; this._div.querySelector('.modal-body').innerHTML = message;
} }
@ -18,21 +37,16 @@ export class ConfirmDialog {
}); });
this._div.querySelectorAll('.close').forEach(function (o){ this._div.querySelectorAll('.close').forEach(function (o){
o.onclick=self.cancel.bind(self); o.onclick=self.close.bind(self);
}); });
this._div.querySelector('.process').onclick = () => {
self._close();
self._resolve();
};
$('.messages-box').appendChild(this._div); $('.messages-box').appendChild(this._div);
return this._promise return this._promise
} }
cancel() { close() {
this._close() this._close()
this._reject('canceled by user'); this._resolve();
} }
_close() { _close() {
@ -43,3 +57,38 @@ export class ConfirmDialog {
export class ConfirmDialog extends Dialog {
template(){
return `
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"></h5>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger close" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary btn-ok process">Process</button>
</div>
</div>
</div>`;
}
show(){
this._div.querySelector('.process').onclick = () => {
self._close();
self._resolve();
};
return super.show()
}
close() {
this._close()
this._reject('canceled by user');
}
}

View file

@ -1,7 +1,7 @@
import 'jquery'; import 'jquery';
import 'bootstrap'; import 'bootstrap';
import 'jquery-form' import 'jquery-form'
import {ConfirmDialog} from './confirm-modal.js'; import {ConfirmDialog, Dialog} from './confirm-modal.js';
jQuery = window.$ = window.jQuery = require('jquery'); jQuery = window.$ = window.jQuery = require('jquery');
var forge = require('node-forge'); var forge = require('node-forge');
@ -39,6 +39,7 @@ function randBase32() {
} }
window.ConfirmDialog = ConfirmDialog; window.ConfirmDialog = ConfirmDialog;
window.Dialog = Dialog;
window.$(document).ready(function () { window.$(document).ready(function () {
$('#sidebarCollapse').onclick = function () { $('#sidebarCollapse').onclick = function () {
@ -58,7 +59,7 @@ window.totp = {
input_secret.value = secret; input_secret.value = secret;
} }
form.querySelector('#name').on('change',window.totp.generate_qrcode); form.querySelector('#name').onchange=window.totp.generate_qrcode;
window.totp.generate_qrcode(); window.totp.generate_qrcode();
}, },
generate_qrcode: function(){ generate_qrcode: function(){
@ -68,7 +69,13 @@ window.totp = {
var issuer = 'Lenticular%20Cloud'; var issuer = 'Lenticular%20Cloud';
var svg_container = $('#svg-container') var svg_container = $('#svg-container')
var svg = new QRCode(`otpauth://totp/${issuer}:${name}?secret=${secret}&issuer=${issuer}`).svg(); var svg = new QRCode(`otpauth://totp/${issuer}:${name}?secret=${secret}&issuer=${issuer}`).svg();
svg_container.html(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;
} }
} }
@ -80,15 +87,30 @@ window.fido2 = {
window.password_change= { window.password_change= {
init: function(){ init: function(){
var form = $('form'); var form = $('form');
form.onsubmit = function () {
SimpleFormSubmit.submitForm(form.action, form) SimpleFormSubmit.submitForm(form.action, form)
.then(response =>{ .then(response =>{
response.json().then(function(data) {
if (data.errors) {
var msg ='<ul>';
for( var field in data.errors) {
msg += `<li>${field}: ${data.errors[field]}</li>`;
}
msg += '</ul>';
new Dialog('Password change Error', `Error Happend: ${msg}`).show()
} else {
new Dialog('Password changed', 'Password changed successfully!').show();
}
}); });
});
return false;
}
} }
} }
window.oauth2_token = { window.oauth2_token = {
revoke: function(href, id){ revoke: function(href, id){
var dialog = new ConfirmDialog(`Are you sure to revoke all tokens from client "${id}"?`); var dialog = new ConfirmDialog('Revoke client tokens', `Are you sure to revoke all tokens from client "${id}"?`);
dialog.show().then(()=>{ dialog.show().then(()=>{
fetch(href, { fetch(href, {
method: 'DELETE' method: 'DELETE'
@ -151,7 +173,7 @@ window.client_cert = {
}); });
}, },
revoke_certificate: function(href, id){ revoke_certificate: function(href, id){
var dialog = new ConfirmDialog(`Are you sure to revoke the certificate with the fingerprint ${id}?`); var dialog = new ConfirmDialog('Revoke client certificate', `Are you sure to revoke the certificate with the fingerprint ${id}?`);
dialog.show().then(()=>{ dialog.show().then(()=>{
fetch(href, { fetch(href, {
method: 'DELETE' method: 'DELETE'

View file

@ -40,7 +40,7 @@ def init_app(name=None):
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']
from .model_db import db from .model import db
db.init_app(app) db.init_app(app)
with app.app_context(): with app.app_context():
db.create_all() db.create_all()

View file

@ -1,9 +1,14 @@
from flask import current_app from flask import current_app
from .form.auth import PasswordForm, TotpForm, Fido2Form from .form.auth import PasswordForm, TotpForm, Fido2Form
from ldap3 import Server, Connection from ldap3 import Server, Connection, HASHED_SALTED_SHA256
from ldap3.core.exceptions import LDAPException from ldap3.core.exceptions import LDAPException
from .model import User
import logging
logger = logging.getLogger(__name__)
import pyotp
class AuthProvider: class AuthProvider:
@ -31,8 +36,9 @@ class LdapAuthProvider(AuthProvider):
return PasswordForm(prefix='password') return PasswordForm(prefix='password')
@staticmethod @staticmethod
def check_auth(user, form): def check_auth(user: User, form):
return LdapAuthProvider.check_auth_internal(user, form.data['password']) return LdapAuthProvider.check_auth_internal(
user, form.data['password'])
@staticmethod @staticmethod
def check_auth_internal(user, password): def check_auth_internal(user, password):

View file

@ -35,9 +35,9 @@ class TOTPDeleteForm(FlaskForm):
class PasswordChangeForm(FlaskForm): class PasswordChangeForm(FlaskForm):
old_password = PasswordField(gettext('Old Password'), validators=[DataRequired()]) password_old = PasswordField(gettext('Old Password'), validators=[DataRequired()])
password = PasswordField(gettext('New Password'), validators=[DataRequired()]) password_new = PasswordField(gettext('New Password'), validators=[DataRequired()])
password_repeat = PasswordField(gettext('Repeat Password'), validators=[DataRequired(),EqualTo('password')]) password_repeat = PasswordField(gettext('Repeat Password'), validators=[DataRequired(),EqualTo('password_new')])
submit = SubmitField(gettext('Change Password')) submit = SubmitField(gettext('Change Password'))
class OidcAuthenticationConfirm(FlaskForm): class OidcAuthenticationConfirm(FlaskForm):

View file

@ -1,8 +1,9 @@
from flask import current_app from flask import current_app
from ldap3_orm import AttrDef, EntryBase as _EntryBase, ObjectDef, EntryType from ldap3_orm import AttrDef, EntryBase as _EntryBase, ObjectDef, EntryType
from ldap3_orm import Reader from ldap3_orm import Reader
from ldap3 import Entry from ldap3 import Entry, HASHED_SALTED_SHA256
from ldap3.utils.conv import escape_filter_chars from ldap3.utils.conv import escape_filter_chars
from ldap3.utils.hashed import hashed
from flask_login import UserMixin from flask_login import UserMixin
from ldap3.core.exceptions import LDAPSessionTerminatedByServerError from ldap3.core.exceptions import LDAPSessionTerminatedByServerError
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
@ -12,10 +13,21 @@ from datetime import datetime
from dateutil import tz from dateutil import tz
import pyotp import pyotp
import json import json
import logging
import crypt
from flask_sqlalchemy import SQLAlchemy, orm
from datetime import datetime
import uuid
import pyotp
logger = logging.getLogger(__name__)
ldap_conn = None # type: Connection ldap_conn = None # type: Connection
base_dn = '' base_dn = ''
db = SQLAlchemy() # type: SQLAlchemy
class SecurityUser(UserMixin): class SecurityUser(UserMixin):
@ -35,16 +47,18 @@ class LambdaStr:
return self.lam() return self.lam()
class EntryBase(object): class EntryBase(db.Model):
__abstract__ = True # for sqlalchemy
_type = None # will get replaced by the local type _type = None # will get replaced by the local type
_query_object = None # will get replaced by the local type _query_object = None # will get replaced by the local type
_base_dn = LambdaStr(lambda: base_dn) _base_dn = LambdaStr(lambda: base_dn)
def __init__(self, ldap_object=None, **kwargs): # def __init__(self, ldap_object=None, **kwargs):
if ldap_object is None: # if ldap_object is None:
self._ldap_object = self.get_type()(**kwargs) # self._ldap_object = self.get_type()(**kwargs)
else: # else:
self._ldap_object = ldap_object # self._ldap_object = ldap_object
def __str__(self): def __str__(self):
return str(self._ldap_object) return str(self._ldap_object)
@ -64,14 +78,17 @@ class EntryBase(object):
return cls._type return cls._type
def commit(self): def commit(self):
self._ldap_object.entry_commit_changes()
def add(self):
print(self._ldap_object.entry_attributes_as_dict) print(self._ldap_object.entry_attributes_as_dict)
ret = ldap_conn.add( ret = ldap_conn.add(
self.dn, self.object_classes, self._ldap_object.entry_attributes_as_dict) self.dn, self.object_classes, self._ldap_object.entry_attributes_as_dict)
print(ret) logger.debug(ret)
pass pass
@classmethod @classmethod
def query(cls): def query_(cls):
if cls._query_object is None: if cls._query_object is None:
cls._query_object = cls._query(cls) cls._query_object = cls._query(cls)
return cls._query_object return cls._query_object
@ -188,77 +205,26 @@ class Certificate(object):
return f'Certificate(cn={self._cn}, ca_name={self._ca_name}, not_valid_before={self.not_valid_before}, not_valid_after={self.not_valid_after})' return f'Certificate(cn={self._cn}, ca_name={self._ca_name}, not_valid_before={self.not_valid_before}, not_valid_after={self.not_valid_after})'
class Totp(object): def generate_uuid():
return str(uuid.uuid4())
def __init__(self, name, secret, created_at=datetime.now()):
self._secret = secret
self._name = name
self._created_at = created_at
@property
def name(self):
return self._name
@property
def created_at(self):
return self._created_at
def verify(self, token: str):
totp = pyotp.TOTP(self._secret)
return totp.verify(token)
def to_dict(self):
return {
'secret': self._secret,
'name': self._name,
'created_at': int(self._created_at.timestamp())}
@staticmethod
def from_dict(data):
return Totp(
name=data['name'],
secret=data['secret'],
created_at=datetime.fromtimestamp(data['created_at']))
class TotpList(MutableSequence):
def __init__(self, ldap_attr):
super().__init__()
self._ldap_attr = ldap_attr
def __getitem__(self, ii):
return Totp.from_dict(json.loads(self._ldap_attr[ii]))
def __setitem__(self, ii, val: Totp):
self._ldap_attr[ii] = json.dumps(val.to_dict()).encode()
def __len__(self):
return len(self._ldap_attr)
def __delitem__(self, ii):
del self._ldap_attr[ii]
def delete(self, totp_name):
for i in range(len(self)):
if self[i].name == totp_name:
self._ldap_attr.delete(self._ldap_attr[i])
def insert(self, ii, val):
self.append(val)
def append(self, val):
self._ldap_attr.add(json.dumps(val.to_dict()).encode())
class User(EntryBase): class User(EntryBase):
id = db.Column(
db.String(length=36), primary_key=True, default=generate_uuid)
username = db.Column(
db.String, unique=True)
totps = db.relationship('Totp', back_populates='user')
dn = "uid={uid},{base_dn}" dn = "uid={uid},{base_dn}"
base_dn = "ou=users,{_base_dn}" base_dn = "ou=users,{_base_dn}"
object_classes = ["top", "inetOrgPerson", "LenticularUser"] object_classes = ["top", "inetOrgPerson", "LenticularUser"]
def __init__(self, ldap_object=None, **kwargs): def __init__(self,**kwargs):
super().__init__(ldap_object, **kwargs) self._ldap_object = None
self._totp_list = TotpList(ldap_object.totpSecret) super(db.Model).__init__(**kwargs)
@property @property
def is_authenticated(self): def is_authenticated(self):
@ -269,24 +235,11 @@ class User(EntryBase):
def make_writeable(self): def make_writeable(self):
self._ldap_object = self._ldap_object.entry_writable() self._ldap_object = self._ldap_object.entry_writable()
self._totp_list = TotpList(self._ldap_object.totpSecret)
@property @property
def entry_dn(self): def entry_dn(self):
return self._ldap_object.entry_dn return self._ldap_object.entry_dn
@property
def username(self):
return self._ldap_object.uid
@username.setter
def username(self, value):
self._ldap_object.uid = value
@property
def userPassword(self):
return self._ldap_object.userPassword
@property @property
def fullname(self): def fullname(self):
return self._ldap_object.fullname return self._ldap_object.fullname
@ -315,14 +268,24 @@ class User(EntryBase):
def gpg_public_key(self): def gpg_public_key(self):
return self._ldap_object.gpgPublicKey return self._ldap_object.gpgPublicKey
@property def change_password(self, password_new: str):
def totps(self): self.make_writeable()
return self._totp_list password_hashed = crypt.crypt(password_new)
self._ldap_object.userPassword = ('{CRYPT}' + password_hashed).encode()
self.commit()
class _query(EntryBase._query): class _query(EntryBase._query):
def _mapping(self, ldap_object): def _mapping(self, ldap_object):
return User(ldap_object=ldap_object) user = User.query.filter(User.username == str(ldap_object.uid)).first()
if user is None:
# migration time
user = User()
user.username = str(ldap_object.uid)
db.session.add(user)
db.session.commit()
user._ldap_object = ldap_object
return user
def by_username(self, username) -> 'User': def by_username(self, username) -> '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)))
@ -333,7 +296,23 @@ class User(EntryBase):
class Totp(db.Model):
id = db.Column(db.Integer, primary_key=True)
secret = db.Column(db.String, nullable=False)
name = db.Column(db.String, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now, nullable=False)
user_id = db.Column(
db.String(length=36),
db.ForeignKey(User.id), nullable=False)
user = db.relationship(User)
def verify(self, token: str):
totp = pyotp.TOTP(self.secret)
return totp.verify(token)
class Group(EntryBase): class Group(EntryBase):
__abstract__ = True # for sqlalchemy, disable for now
dn = "cn={cn},{base_dn}" dn = "cn={cn},{base_dn}"
base_dn = "ou=Users,{_base_dn}" base_dn = "ou=Users,{_base_dn}"
object_classes = ["top"] object_classes = ["top"]

View file

@ -1,5 +1,7 @@
from flask_sqlalchemy import SQLAlchemy, orm from flask_sqlalchemy import SQLAlchemy, orm
from datetime import datetime
import uuid import uuid
import pyotp
db = SQLAlchemy() # type: SQLAlchemy db = SQLAlchemy() # type: SQLAlchemy
@ -8,39 +10,26 @@ def generate_uuid():
return str(uuid.uuid4()) return str(uuid.uuid4())
class OAuth(db.Model):
token = db.Column(db.Text, primary_key=True)
provider = db.Column(db.Text)
provider_username = db.Column(db.Text)
class User(db.Model): class User(db.Model):
id = db.Column(db.String(length=36), primary_key=True, default=generate_uuid) id = db.Column(
username = db.Column(db.String, unique=True) db.String(length=36), primary_key=True, default=generate_uuid)
username = db.Column(
db.String, unique=True)
totps = db.relationship('Totp', back_populates='user')
class Client(db.Model): class Totp(object):
key = db.Column(db.Text, primary_key=True) id = db.Column(db.Integer, primary_key=True)
value = db.Column(db.Text) secret = db.Column(db.String, nullable=False)
name = db.Column(db.String, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now, nullable=False)
class AuthzCode(db.Model):
key = db.Column(db.Text, primary_key=True)
value = db.Column(db.Text)
class AccessToken(db.Model):
key = db.Column(db.Text, primary_key=True)
value = db.Column(db.Text)
class RefreshToken(db.Model):
key = db.Column(db.Text, primary_key=True)
value = db.Column(db.Text)
class SubjectIdentifier(db.Model):
key = db.Column(db.Text, primary_key=True)
value = db.Column(db.Text)
user_id = db.Column(
db.Integer,
db.ForeignKey(User.id), nullable=False)
user = db.relationship(User)
def verify(self, token: str):
totp = pyotp.TOTP(self._secret)
return totp.verify(token)

View file

@ -11,7 +11,6 @@ import logging
import requests import requests
from ..model import User from ..model import User
from ..model_db import User as DbUser
from ..auth_providers import LdapAuthProvider from ..auth_providers import LdapAuthProvider
@ -19,11 +18,15 @@ api_views = Blueprint('api', __name__, url_prefix='/api')
@api_views.route('/userinfo', methods=['GET', 'POST']) @api_views.route('/userinfo', methods=['GET', 'POST'])
def userinfo(): def userinfo():
if 'authorization' not in request.headers:
return 'not token found', 400
token = request.headers['authorization'].replace('Bearer ', '') token = request.headers['authorization'].replace('Bearer ', '')
token_info = current_app.hydra_api.introspect_o_auth2_token(token=token) token_info = current_app.hydra_api.introspect_o_auth2_token(token=token)
if not token_info.active:
return 'token not valid', 403
user_db = DbUser.query.get(token_info.sub) user_db = User.query.get(token_info.sub)
user = User.query().by_username(user_db.username) user = User.query_().by_username(user_db.username)
public_url = current_app.config.get('HYDRA_PUBLIC_URL') public_url = current_app.config.get('HYDRA_PUBLIC_URL')
r = requests.get( r = requests.get(
@ -50,5 +53,6 @@ def user_list():
if 'lc_i_userlist' not in token_info.scope.split(' '): if 'lc_i_userlist' not in token_info.scope.split(' '):
return '', 403 return '', 403
return jsonify([{'username': str(user.username), 'email': str(user.email)} return jsonify([
for user in User.query().all()]) {'username': str(user.username), 'email': str(user.email)}
for user in User.query_().all()])

View file

@ -14,8 +14,7 @@ from urllib.parse import urlparse
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
import http import http
from ..model import User, SecurityUser from ..model import db, User, SecurityUser
from ..model_db import db, User as DbUser
from ..form.auth import ConsentForm, LoginForm from ..form.auth import ConsentForm, LoginForm
from ..auth_providers import AUTH_PROVIDER_LIST from ..auth_providers import AUTH_PROVIDER_LIST
@ -29,7 +28,7 @@ def consent():
# DUMMPY ONLY # DUMMPY ONLY
form = ConsentForm() form = ConsentForm()
remember_for = 60*60*24*7 # remember for 7 days remember_for = 60*60*24*30 # remember for 7 days
consent_request = current_app.hydra_api.get_consent_request( consent_request = current_app.hydra_api.get_consent_request(
request.args['consent_challenge']) request.args['consent_challenge'])
@ -42,7 +41,7 @@ def consent():
'grant_scope': requested_scope, 'grant_scope': requested_scope,
'grant_access_token_audience': requested_audiences, 'grant_access_token_audience': requested_audiences,
'remember': form.data['remember'], 'remember': form.data['remember'],
# 'remember_for': remember_for, 'remember_for': remember_for,
}) })
return redirect(resp.redirect_to) return redirect(resp.redirect_to)
return render_template( return render_template(
@ -65,7 +64,7 @@ def login():
return redirect(resp.redirect_to) return redirect(resp.redirect_to)
form = LoginForm() form = LoginForm()
if form.validate_on_submit(): if form.validate_on_submit():
user = User.query().by_username(form.data['name']) user = User.query_().by_username(form.data['name'])
if user: if user:
session['username'] = str(user.username) session['username'] = str(user.username)
else: else:
@ -83,7 +82,7 @@ def login_auth():
if 'username' not in session: if 'username' not in session:
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
auth_forms = {} auth_forms = {}
user = User.query().by_username(session['username']) user = User.query_().by_username(session['username'])
for auth_provider in AUTH_PROVIDER_LIST: for auth_provider in AUTH_PROVIDER_LIST:
form = auth_provider.get_form() form = auth_provider.get_form()
if auth_provider.get_name() not in session['auth_providers'] and\ if auth_provider.get_name() not in session['auth_providers'] and\
@ -95,31 +94,18 @@ def login_auth():
if len(session['auth_providers']) >= 2: if len(session['auth_providers']) >= 2:
remember_me = True remember_me = True
db_user = DbUser.query.filter(DbUser.username == session['username']).one_or_none() # if db_user is None:
if db_user is None: # db_user = User(username=session['username'])
db_user = DbUser(username=session['username']) # db.session.add(db_user)
db.session.add(db_user) # db.session.commit()
db.session.commit()
subject = db_user.id subject = user.id
resp = current_app.hydra_api.accept_login_request( resp = current_app.hydra_api.accept_login_request(
login_challenge, body={ login_challenge, body={
'subject': subject, 'subject': subject,
'remember': remember_me}) 'remember': remember_me})
return redirect(resp.redirect_to) return redirect(resp.redirect_to)
login_user(SecurityUser(session['username']))
# TODO use this var
_next = None
try:
_next_url = urlparse(b64decode(request.args.get('next')).decode())
_host_url = urlparse(request.url)
if _next_url.scheme == _host_url.scheme and _next_url.netloc == _host_url.netloc :
_next = _next_url.geturl()
except TypeError:
_next = None
return redirect(_next or url_for('frontend.index'))
return render_template('auth/login_auth.html.j2', forms=auth_forms) return render_template('auth/login_auth.html.j2', forms=auth_forms)
@ -128,7 +114,7 @@ def logout():
logout_challenge = request.args.get('logout_challenge') logout_challenge = request.args.get('logout_challenge')
logout_request = current_app.hydra_api.get_logout_request(logout_challenge) logout_request = current_app.hydra_api.get_logout_request(logout_challenge)
resp = current_app.hydra_api.accept_logout_request(logout_challenge) resp = current_app.hydra_api.accept_logout_request(logout_challenge)
logout_user() # TODO confirm
return redirect(resp.redirect_to) return redirect(resp.redirect_to)

View file

@ -1,46 +1,47 @@
from urllib.parse import urlencode, parse_qs from urllib.parse import urlencode, parse_qs
import flask
from flask import Blueprint, redirect from flask import Blueprint, redirect
from flask import current_app, session from flask import current_app
from flask import jsonify, send_file from flask import jsonify
from flask.helpers import make_response from flask import render_template, url_for, flash
from flask.templating import render_template from flask_login import login_user, logout_user, current_user
from oic.oic.message import TokenErrorResponse, UserInfoErrorResponse, EndSessionRequest
from pyop.access_token import AccessToken, BearerTokenError
from pyop.exceptions import InvalidAuthenticationRequest, InvalidAccessToken, InvalidClientAuthentication, OAuthError, \
InvalidSubjectIdentifier, InvalidClientRegistrationRequest
from pyop.util import should_fragment_encode
from flask import Blueprint, render_template, request, url_for, flash
from flask_login import login_required, login_user, logout_user, current_user
from werkzeug.utils import redirect from werkzeug.utils import redirect
import logging import logging
from datetime import timedelta from datetime import timedelta
import pyotp from base64 import b64decode
from base64 import b64decode, b64encode
from flask_dance.consumer import oauth_authorized from flask_dance.consumer import oauth_authorized
from sqlalchemy.orm.exc import NoResultFound
from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer import OAuth2ConsumerBlueprint
from oauthlib.oauth2.rfc6749.errors import TokenExpiredError
from ..model import User, SecurityUser, Totp from ..model import db, User, SecurityUser, Totp
from ..model_db import OAuth, db, User as DbUser from ..form.frontend import ClientCertForm, TOTPForm, \
from ..form.frontend import ClientCertForm, TOTPForm, TOTPDeleteForm, PasswordChangeForm TOTPDeleteForm, PasswordChangeForm
from ..auth_providers import AUTH_PROVIDER_LIST from ..auth_providers import LdapAuthProvider
frontend_views = Blueprint('frontend', __name__, url_prefix='') frontend_views = Blueprint('frontend', __name__, url_prefix='')
logger = logging.getLogger(__name__)
def before_request():
try:
resp = current_app.oauth.session.get('/userinfo')
if not current_user.is_authenticated:
return redirect(url_for('oauth.login'))
except TokenExpiredError:
return redirect(url_for('oauth.login'))
frontend_views.before_request(before_request)
def init_login_manager(app): def init_login_manager(app):
@app.login_manager.user_loader @app.login_manager.user_loader
def user_loader(username): def user_loader(username):
return User.query().by_username(username) return User.query_().by_username(username)
@app.login_manager.request_loader @app.login_manager.request_loader
def request_loader(request): def request_loader(_request):
pass pass
@app.login_manager.unauthorized_handler @app.login_manager.unauthorized_handler
@ -75,25 +76,9 @@ def init_login_manager(app):
oauth_info = resp.json() oauth_info = resp.json()
db_user = DbUser.query.get(str(oauth_info["sub"])) db_user = User.query.get(str(oauth_info["sub"]))
oauth_username = db_user.username
# Find this OAuth token in the database, or create it login_user(SecurityUser(db_user.username))
query = OAuth.query.filter_by(
provider=blueprint.name,
provider_username=oauth_username,
)
try:
oauth = query.one()
except NoResultFound:
oauth = OAuth(
provider=blueprint.name,
provider_username=oauth_username,
token=token,
)
login_user(SecurityUser(oauth.provider_username))
#flash("Successfully signed in with GitHub.") #flash("Successfully signed in with GitHub.")
# Since we're manually creating the OAuth model in the database, # Since we're manually creating the OAuth model in the database,
@ -106,27 +91,29 @@ def init_login_manager(app):
@frontend_views.route('/logout') @frontend_views.route('/logout')
def logout(): def logout():
logout_user() logout_user()
return redirect(f'{current_app.config["HYDRA_PUBLIC_URL"]}/oauth2/sessions/logout') return redirect(
f'{current_app.config["HYDRA_PUBLIC_URL"]}/oauth2/sessions/logout')
@frontend_views.route('/', methods=['GET']) @frontend_views.route('/', methods=['GET'])
@login_required
def index(): def index():
return render_template('frontend/index.html.j2') return render_template('frontend/index.html.j2')
@frontend_views.route('/client_cert') @frontend_views.route('/client_cert')
@login_required
def client_cert(): def client_cert():
client_certs = {} client_certs = {}
for service in current_app.lenticular_services.values(): for service in current_app.lenticular_services.values():
client_certs[str(service.name)] = current_app.pki.get_client_certs(current_user, service) client_certs[str(service.name)] = \
current_app.pki.get_client_certs(current_user, service)
return render_template('frontend/client_cert.html.j2', services=current_app.lenticular_services, client_certs=client_certs) return render_template(
'frontend/client_cert.html.j2',
services=current_app.lenticular_services,
client_certs=client_certs)
@frontend_views.route('/client_cert/<service_name>/<serial_number>') @frontend_views.route('/client_cert/<service_name>/<serial_number>')
@login_required
def get_client_cert(service_name, serial_number): def get_client_cert(service_name, serial_number):
service = current_app.lenticular_services[service_name] service = current_app.lenticular_services[service_name]
cert = current_app.pki.get_client_cert( cert = current_app.pki.get_client_cert(
@ -137,8 +124,8 @@ def get_client_cert(service_name, serial_number):
}) })
@frontend_views.route('/client_cert/<service_name>/<serial_number>', methods=['DELETE']) @frontend_views.route(
@login_required '/client_cert/<service_name>/<serial_number>', methods=['DELETE'])
def revoke_client_cert(service_name, serial_number): def revoke_client_cert(service_name, serial_number):
service = current_app.lenticular_services[service_name] service = current_app.lenticular_services[service_name]
cert = current_app.pki.get_client_cert( cert = current_app.pki.get_client_cert(
@ -150,7 +137,6 @@ def revoke_client_cert(service_name, serial_number):
@frontend_views.route( @frontend_views.route(
'/client_cert/<service_name>/new', '/client_cert/<service_name>/new',
methods=['GET', 'POST']) methods=['GET', 'POST'])
@login_required
def client_cert_new(service_name): def client_cert_new(service_name):
service = current_app.lenticular_services[service_name] service = current_app.lenticular_services[service_name]
form = ClientCertForm() form = ClientCertForm()
@ -180,23 +166,20 @@ def client_cert_new(service_name):
@frontend_views.route('/totp') @frontend_views.route('/totp')
@login_required
def totp(): def totp():
delete_form = TOTPDeleteForm() delete_form = TOTPDeleteForm()
return render_template('frontend/totp.html.j2', delete_form=delete_form) return render_template('frontend/totp.html.j2', delete_form=delete_form)
@frontend_views.route('/totp/new', methods=['GET', 'POST']) @frontend_views.route('/totp/new', methods=['GET', 'POST'])
@login_required
def totp_new(): def totp_new():
form = TOTPForm() form = TOTPForm()
if form.validate_on_submit(): if form.validate_on_submit():
totp = Totp(name=form.data['name'], secret=form.data['secret']) totp = Totp(name=form.data['name'], secret=form.data['secret'])
if totp.verify(form.data['token']): if totp.verify(form.data['token']):
current_user.make_writeable()
current_user.totps.append(totp) current_user.totps.append(totp)
current_user._ldap_object.entry_commit_changes() db.session.commit()
return jsonify({ return jsonify({
'status': 'ok'}) 'status': 'ok'})
else: else:
@ -208,47 +191,55 @@ def totp_new():
return render_template('frontend/totp_new.html.j2', form=form) return render_template('frontend/totp_new.html.j2', form=form)
@frontend_views.route('/totp/<totp_name>/delete', methods=['GET','POST']) @frontend_views.route('/totp/<totp_name>/delete', methods=['GET', 'POST'])
@login_required
def totp_delete(totp_name): def totp_delete(totp_name):
current_user.make_writeable() totp = Totp.query.filter(Totp.name == totp_name).first()
current_user.totps.delete(totp_name) db.session.delete(totp)
current_user._ldap_object.entry_commit_changes() db.session.commit()
return jsonify({ return jsonify({
'status': 'ok'}) 'status': 'ok'})
@frontend_views.route('/password_change') @frontend_views.route('/password_change')
@login_required
def password_change(): def password_change():
form = PasswordChangeForm() form = PasswordChangeForm()
return render_template('frontend/password_change.html.j2', form=form) return render_template('frontend/password_change.html.j2', form=form)
@frontend_views.route('/password_change', methods=['POST']) @frontend_views.route('/password_change', methods=['POST'])
@login_required
def password_change_post(): def password_change_post():
form = PasswordChangeForm() form = PasswordChangeForm()
if form.validate(): if form.validate():
password_old = str(form.data['password_old'])
password_new = str(form.data['password_new'])
if not LdapAuthProvider.check_auth_internal(
current_user, password_old):
return jsonify(
{'errors': {'password_old': 'Old Password is invalid'}})
resp = current_user.change_password(password_new)
if resp:
print(current_user)
return jsonify({}) return jsonify({})
else:
return jsonify({'errors': {'internal': 'internal server errror'}})
return jsonify({'errors': form.errors}) return jsonify({'errors': form.errors})
@frontend_views.route('/oauth2_token') @frontend_views.route('/oauth2_token')
@login_required
def oauth2_tokens(): def oauth2_tokens():
subject = current_app.oauth.session.get('/userinfo').json()['sub'] subject = current_app.oauth.session.get('/userinfo').json()['sub']
consent_sessions = current_app.hydra_api.list_subject_consent_sessions( consent_sessions = current_app.hydra_api.list_subject_consent_sessions(
subject) subject)
return render_template('frontend/oauth2_tokens.html.j2', consent_sessions=consent_sessions) print(consent_sessions)
return render_template(
'frontend/oauth2_tokens.html.j2',
consent_sessions=consent_sessions)
@frontend_views.route('/oauth2_token/<client_id>', methods=['DELETE']) @frontend_views.route('/oauth2_token/<client_id>', methods=['DELETE'])
@login_required
def oauth2_token_revoke(client_id: str): def oauth2_token_revoke(client_id: str):
subject = current_app.oauth.session.get('/userinfo').json()['sub'] subject = current_app.oauth.session.get('/userinfo').json()['sub']
current_app.hydra_api.revoke_consent_sessions( current_app.hydra_api.revoke_consent_sessions(

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -18,18 +18,6 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="row"> <div class="row">
{% if current_user.is_authenticated %}
<nav class="col-md-2 d-none d-md-block bg-light sidebar fixed-top">
<div class="sidebar-sticky active">
{#<a href="/"><img alt="logo" class="container-fluid" src="/static/images/dog_main_small.png"></a>#}
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.index') }}">{{ gettext('Account') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.client_cert') }}">{{ gettext('Client Cert') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.totp') }}">{{ gettext('2FA - TOTP') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.logout') }}">{{ gettext('Logout') }}</a></li>
</div>
</nav>
{% endif %}
<main class="col-md-9 ml-sm-auto col-lg-10 px-4" role="main"> <main class="col-md-9 ml-sm-auto col-lg-10 px-4" role="main">
<h1>{% block title %}{% endblock %}</h1> <h1>{% block title %}{% endblock %}</h1>
<div class="card"> <div class="card">

View file

@ -6,4 +6,11 @@
{{ render_form(form)}} {{ render_form(form)}}
{% endblock %}
{% block script_js %}
password_change.init();
{% endblock %} {% endblock %}