2020-05-09 18:00:07 +00:00
|
|
|
|
2022-02-19 22:16:13 +00:00
|
|
|
from authlib.integrations.base_client.errors import MissingTokenError, InvalidTokenError
|
2020-05-09 18:00:07 +00:00
|
|
|
from urllib.parse import urlencode, parse_qs
|
2020-06-02 17:09:32 +00:00
|
|
|
from flask import Blueprint, redirect, request
|
2020-05-27 15:56:10 +00:00
|
|
|
from flask import current_app
|
2020-06-02 17:09:32 +00:00
|
|
|
from flask import jsonify, session
|
2022-02-19 22:16:13 +00:00
|
|
|
from flask import render_template, url_for
|
2020-05-27 15:56:10 +00:00
|
|
|
from flask_login import login_user, logout_user, current_user
|
2020-05-09 18:00:07 +00:00
|
|
|
from werkzeug.utils import redirect
|
|
|
|
import logging
|
|
|
|
from datetime import timedelta
|
2020-05-27 15:56:10 +00:00
|
|
|
from base64 import b64decode
|
2022-02-19 22:16:13 +00:00
|
|
|
from flask.typing import ResponseReturnValue
|
2020-05-27 15:56:10 +00:00
|
|
|
from oauthlib.oauth2.rfc6749.errors import TokenExpiredError
|
2022-02-19 22:16:13 +00:00
|
|
|
from ory_hydra_client.api.admin import list_subject_consent_sessions, revoke_consent_sessions
|
|
|
|
from ory_hydra_client.models import GenericError
|
|
|
|
from typing import Optional
|
2020-05-09 18:00:07 +00:00
|
|
|
|
2020-05-27 15:56:10 +00:00
|
|
|
from ..model import db, User, SecurityUser, Totp
|
|
|
|
from ..form.frontend import ClientCertForm, TOTPForm, \
|
|
|
|
TOTPDeleteForm, PasswordChangeForm
|
|
|
|
from ..auth_providers import LdapAuthProvider
|
2022-02-19 22:16:13 +00:00
|
|
|
from .oauth2 import redirect_login, oauth2
|
|
|
|
from ..hydra import hydra_service
|
2020-05-09 18:00:07 +00:00
|
|
|
|
|
|
|
frontend_views = Blueprint('frontend', __name__, url_prefix='')
|
2020-05-27 15:56:10 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2022-02-19 22:16:13 +00:00
|
|
|
def before_request() -> Optional[ResponseReturnValue]:
|
2020-05-27 15:56:10 +00:00
|
|
|
try:
|
2022-02-19 22:16:13 +00:00
|
|
|
resp = oauth2.custom.get('/userinfo')
|
2022-02-06 22:57:01 +00:00
|
|
|
if not current_user.is_authenticated or resp.status_code != 200:
|
2020-06-21 09:52:37 +00:00
|
|
|
logger.info('user not logged in redirect')
|
2020-06-02 17:09:32 +00:00
|
|
|
return redirect_login()
|
2022-02-19 22:16:13 +00:00
|
|
|
except MissingTokenError:
|
|
|
|
return redirect_login()
|
|
|
|
except InvalidTokenError:
|
2020-06-02 17:09:32 +00:00
|
|
|
return redirect_login()
|
2020-05-27 15:56:10 +00:00
|
|
|
|
2022-02-19 22:16:13 +00:00
|
|
|
return None
|
2020-05-27 15:56:10 +00:00
|
|
|
|
2020-05-09 18:00:07 +00:00
|
|
|
|
2022-02-19 22:16:13 +00:00
|
|
|
frontend_views.before_request(before_request)
|
2020-05-21 11:20:27 +00:00
|
|
|
|
2020-06-02 17:09:32 +00:00
|
|
|
|
|
|
|
@frontend_views.route('/logout')
|
2022-02-19 22:16:13 +00:00
|
|
|
def logout() -> ResponseReturnValue:
|
2020-06-02 17:09:32 +00:00
|
|
|
logout_user()
|
|
|
|
return redirect(
|
|
|
|
f'{current_app.config["HYDRA_PUBLIC_URL"]}/oauth2/sessions/logout')
|
2020-05-21 11:20:27 +00:00
|
|
|
|
|
|
|
|
2020-05-09 18:00:07 +00:00
|
|
|
@frontend_views.route('/', methods=['GET'])
|
2022-02-19 22:16:13 +00:00
|
|
|
def index() -> ResponseReturnValue:
|
2020-06-02 17:09:32 +00:00
|
|
|
if 'next_url' in session:
|
|
|
|
next_url = session['next_url']
|
|
|
|
del session['next_url']
|
|
|
|
return redirect(next_url)
|
2020-05-09 18:00:07 +00:00
|
|
|
return render_template('frontend/index.html.j2')
|
|
|
|
|
|
|
|
|
|
|
|
@frontend_views.route('/client_cert')
|
2022-02-19 22:16:13 +00:00
|
|
|
def client_cert() -> ResponseReturnValue:
|
2020-05-09 18:00:07 +00:00
|
|
|
client_certs = {}
|
|
|
|
for service in current_app.lenticular_services.values():
|
2020-05-27 15:56:10 +00:00
|
|
|
client_certs[str(service.name)] = \
|
|
|
|
current_app.pki.get_client_certs(current_user, service)
|
2020-05-09 18:00:07 +00:00
|
|
|
|
2020-05-27 15:56:10 +00:00
|
|
|
return render_template(
|
|
|
|
'frontend/client_cert.html.j2',
|
|
|
|
services=current_app.lenticular_services,
|
|
|
|
client_certs=client_certs)
|
2020-05-09 18:00:07 +00:00
|
|
|
|
|
|
|
|
2020-05-25 18:23:27 +00:00
|
|
|
@frontend_views.route('/client_cert/<service_name>/<serial_number>')
|
2022-02-19 22:16:13 +00:00
|
|
|
def get_client_cert(service_name, serial_number) -> ResponseReturnValue:
|
2020-05-09 18:00:07 +00:00
|
|
|
service = current_app.lenticular_services[service_name]
|
2020-05-25 18:23:27 +00:00
|
|
|
cert = current_app.pki.get_client_cert(
|
|
|
|
current_user, service, serial_number)
|
|
|
|
return jsonify({
|
|
|
|
'data': {
|
|
|
|
'pem': cert.pem()}
|
|
|
|
})
|
|
|
|
|
|
|
|
|
2020-05-27 15:56:10 +00:00
|
|
|
@frontend_views.route(
|
|
|
|
'/client_cert/<service_name>/<serial_number>', methods=['DELETE'])
|
2022-02-19 22:16:13 +00:00
|
|
|
def revoke_client_cert(service_name, serial_number) -> ResponseReturnValue:
|
2020-05-25 18:23:27 +00:00
|
|
|
service = current_app.lenticular_services[service_name]
|
|
|
|
cert = current_app.pki.get_client_cert(
|
|
|
|
current_user, service, serial_number)
|
|
|
|
current_app.pki.revoke_certificate(cert)
|
|
|
|
return jsonify({})
|
2020-05-09 18:00:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
@frontend_views.route(
|
|
|
|
'/client_cert/<service_name>/new',
|
|
|
|
methods=['GET', 'POST'])
|
2022-02-19 22:16:13 +00:00
|
|
|
def client_cert_new(service_name) -> ResponseReturnValue:
|
2020-05-09 18:00:07 +00:00
|
|
|
service = current_app.lenticular_services[service_name]
|
|
|
|
form = ClientCertForm()
|
|
|
|
if form.validate_on_submit():
|
|
|
|
valid_time = int(form.data['valid_time']) * timedelta(1, 0, 0)
|
|
|
|
cert = current_app.pki.signing_publickey(
|
|
|
|
current_user,
|
|
|
|
service,
|
|
|
|
form.data['publickey'],
|
|
|
|
valid_time=valid_time)
|
2020-05-10 12:34:28 +00:00
|
|
|
return jsonify({
|
2020-05-09 18:00:07 +00:00
|
|
|
'status': 'ok',
|
|
|
|
'data': {
|
|
|
|
'cert': cert.pem(),
|
|
|
|
'ca_cert': current_app.pki.get_ca_cert_pem(service)
|
|
|
|
}})
|
|
|
|
elif form.is_submitted():
|
|
|
|
return jsonify({
|
|
|
|
'status': 'error',
|
|
|
|
'errors': form.errors
|
|
|
|
})
|
|
|
|
|
2020-05-25 18:23:27 +00:00
|
|
|
return render_template(
|
|
|
|
'frontend/client_cert_new.html.j2',
|
2020-05-09 18:00:07 +00:00
|
|
|
service=service,
|
|
|
|
form=form)
|
|
|
|
|
|
|
|
|
|
|
|
@frontend_views.route('/totp')
|
2022-02-19 22:16:13 +00:00
|
|
|
def totp() -> ResponseReturnValue:
|
2020-05-10 12:34:28 +00:00
|
|
|
delete_form = TOTPDeleteForm()
|
|
|
|
return render_template('frontend/totp.html.j2', delete_form=delete_form)
|
|
|
|
|
2020-05-09 18:00:07 +00:00
|
|
|
|
2020-05-25 18:23:27 +00:00
|
|
|
@frontend_views.route('/totp/new', methods=['GET', 'POST'])
|
2022-02-19 22:16:13 +00:00
|
|
|
def totp_new() -> ResponseReturnValue:
|
2020-05-10 12:34:28 +00:00
|
|
|
form = TOTPForm()
|
|
|
|
|
|
|
|
if form.validate_on_submit():
|
|
|
|
totp = Totp(name=form.data['name'], secret=form.data['secret'])
|
|
|
|
if totp.verify(form.data['token']):
|
|
|
|
current_user.totps.append(totp)
|
2020-05-27 15:56:10 +00:00
|
|
|
db.session.commit()
|
2020-05-10 12:34:28 +00:00
|
|
|
return jsonify({
|
|
|
|
'status': 'ok'})
|
|
|
|
else:
|
|
|
|
return jsonify({
|
|
|
|
'status': 'error',
|
|
|
|
'errors': [
|
|
|
|
'TOTP Token invalid'
|
|
|
|
]})
|
|
|
|
return render_template('frontend/totp_new.html.j2', form=form)
|
|
|
|
|
|
|
|
|
2020-05-27 15:56:10 +00:00
|
|
|
@frontend_views.route('/totp/<totp_name>/delete', methods=['GET', 'POST'])
|
2022-02-19 22:16:13 +00:00
|
|
|
def totp_delete(totp_name) -> ResponseReturnValue:
|
2020-05-27 15:56:10 +00:00
|
|
|
totp = Totp.query.filter(Totp.name == totp_name).first()
|
|
|
|
db.session.delete(totp)
|
|
|
|
db.session.commit()
|
2020-05-09 18:00:07 +00:00
|
|
|
|
2020-05-10 12:34:28 +00:00
|
|
|
return jsonify({
|
|
|
|
'status': 'ok'})
|
2020-05-26 20:55:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
@frontend_views.route('/password_change')
|
2022-02-19 22:16:13 +00:00
|
|
|
def password_change() -> ResponseReturnValue:
|
2020-05-26 20:55:37 +00:00
|
|
|
form = PasswordChangeForm()
|
|
|
|
return render_template('frontend/password_change.html.j2', form=form)
|
|
|
|
|
|
|
|
|
|
|
|
@frontend_views.route('/password_change', methods=['POST'])
|
2022-02-19 22:16:13 +00:00
|
|
|
def password_change_post() -> ResponseReturnValue:
|
2020-05-26 20:55:37 +00:00
|
|
|
form = PasswordChangeForm()
|
|
|
|
if form.validate():
|
2020-05-27 15:56:10 +00:00
|
|
|
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:
|
|
|
|
return jsonify({})
|
|
|
|
else:
|
|
|
|
return jsonify({'errors': {'internal': 'internal server errror'}})
|
2020-05-26 20:55:37 +00:00
|
|
|
return jsonify({'errors': form.errors})
|
|
|
|
|
|
|
|
|
|
|
|
@frontend_views.route('/oauth2_token')
|
2022-02-19 22:16:13 +00:00
|
|
|
def oauth2_tokens() -> ResponseReturnValue:
|
2020-05-26 20:55:37 +00:00
|
|
|
|
2022-02-19 22:16:13 +00:00
|
|
|
subject = oauth2.custom.get('/userinfo').json()['sub']
|
|
|
|
consent_sessions = list_subject_consent_sessions.sync(subject=subject, _client=hydra_service.hydra_client)
|
|
|
|
if consent_sessions is None or isinstance( consent_sessions, GenericError):
|
|
|
|
return 'internal error, could not fetch sessions', 500
|
2020-05-27 15:56:10 +00:00
|
|
|
return render_template(
|
|
|
|
'frontend/oauth2_tokens.html.j2',
|
|
|
|
consent_sessions=consent_sessions)
|
|
|
|
|
2020-05-26 20:55:37 +00:00
|
|
|
|
|
|
|
@frontend_views.route('/oauth2_token/<client_id>', methods=['DELETE'])
|
2022-02-19 22:16:13 +00:00
|
|
|
def oauth2_token_revoke(client_id: str) -> ResponseReturnValue:
|
|
|
|
subject = oauth2.session.get('/userinfo').json()['sub']
|
|
|
|
revoke_consent_sessions.sync( _client=hydra_service.hydra_client,
|
|
|
|
subject=subject,
|
2020-05-26 20:55:37 +00:00
|
|
|
client=client_id)
|
|
|
|
|
|
|
|
return jsonify({})
|