init commit

This commit is contained in:
TuxCoder 2020-05-09 20:00:07 +02:00
commit dfd166bd3b
55 changed files with 18538 additions and 0 deletions

View file

173
lenticular_cloud/app.py Normal file
View file

@ -0,0 +1,173 @@
from flask.app import Flask
from flask import g
from flask.helpers import url_for
from jwkest.jwk import RSAKey, rsa_load
from flask_babel import Babel
from flask_login import LoginManager
import time
from pyop.authz_state import AuthorizationState
from pyop.provider import Provider
from pyop.subject_identifier import HashBasedSubjectIdentifierFactory
from pyop.userinfo import Userinfo as _Userinfo
from ldap3 import Connection, Server, ALL
from . import model
from .pki import Pki
def init_oidc_provider(app):
with app.app_context():
issuer = url_for('frontend.index')[:-1]
authentication_endpoint = url_for('oidc_provider.authentication_endpoint')
jwks_uri = url_for('oidc_provider.jwks_uri')
token_endpoint = url_for('oidc_provider.token_endpoint')
userinfo_endpoint = url_for('oidc_provider.userinfo_endpoint')
registration_endpoint = url_for('oidc_provider.registration_endpoint')
end_session_endpoint = url_for('auth.logout')
configuration_information = {
'issuer': issuer,
'authorization_endpoint': authentication_endpoint,
'jwks_uri': jwks_uri,
'token_endpoint': token_endpoint,
'userinfo_endpoint': userinfo_endpoint,
'registration_endpoint': registration_endpoint,
'end_session_endpoint': end_session_endpoint,
'scopes_supported': ['openid', 'profile'],
'response_types_supported': ['code', 'code id_token', 'code token', 'code id_token token'], # code and hybrid
'response_modes_supported': ['query', 'fragment'],
'grant_types_supported': ['authorization_code', 'implicit'],
'subject_types_supported': ['pairwise'],
'token_endpoint_auth_methods_supported': ['client_secret_basic', 'client_secret_post'],
'claims_parameter_supported': True
}
from .model_db import db, Client, AuthzCode, AccessToken, RefreshToken, SubjectIdentifier
from .model import User
import json
db.init_app(app)
with app.app_context():
db.create_all()
class SqlAlchemyWrapper(object):
def __init__(self, cls):
self._cls = cls
pass
def __getitem__(self, item):
o = self._cls.query.get(item)
if o is not None:
return json.loads(o.value)
else:
raise KeyError()
def __setitem__(self, item, value):
o = self._cls.query.get(item)
if o is None:
o = self._cls(key=item)
db.session.add(o)
o.value = json.dumps(value)
db.session.commit()
def items(self):
aa = self._cls.query.all()
return [(a.key, json.loads(a.value)) for a in aa]
def __contains__(self, item):
return self._cls.query.get(item) is not None
class Userinfo(_Userinfo):
def __init__(self):
pass
def __getitem__(self, item):
return User.query().by_username(item)
def __contains__(self, item):
return User.query().by_username(item) is not None
def get_claims_for(self, user_id, requested_claims):
user = self[user_id]
print(f'user {user.username}')
claims = {}
for claim in requested_claims:
if claim == 'name':
claims[claim] = str(user.username)
elif claim == 'email':
claims[claim] = str(user.mail)
elif claim == 'email_verified':
claims[claim] = True
else:
print(f'claim not found {claim}')
return claims
client_db = SqlAlchemyWrapper(Client)
userinfo_db = Userinfo()
signing_key = RSAKey(key=rsa_load('signing_key.pem'), alg='RS256')
provider = Provider(
signing_key,
configuration_information,
AuthorizationState(
HashBasedSubjectIdentifierFactory(app.config['SUBJECT_ID_HASH_SALT']),
SqlAlchemyWrapper(AuthzCode),
SqlAlchemyWrapper(AccessToken),
SqlAlchemyWrapper(RefreshToken),
SqlAlchemyWrapper(SubjectIdentifier)
),
client_db,
userinfo_db)
return provider
def oidc_provider_init_app(name=None):
name = name or __name__
app = Flask(name)
app.config.from_pyfile('application.cfg')
app.config.from_pyfile('production.cfg')
#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)
app.ldap_conn = Connection(server, app.config['LDAP_BIND_DN'], app.config['LDAP_BIND_PW'], auto_bind=True)
model.ldap_conn = app.ldap_conn
model.base_dn = app.config['LDAP_BASE_DN']
app.babel = Babel(app)
app.login_manager = LoginManager(app)
init_login_manager(app)
from .views import oidc_provider_views, auth_views, frontend_views
app.register_blueprint(oidc_provider_views)
app.register_blueprint(auth_views)
app.register_blueprint(frontend_views)
@app.before_request
def befor_request():
request_start_time = time.time()
g.request_time = lambda: "%.5fs" % (time.time() - request_start_time)
# Initialize the oidc_provider after views to be able to set correct urls
app.provider = init_oidc_provider(app)
from .translations import init_babel
init_babel(app)
app.lenticular_services = {}
for service_name, service_config in app.config['LENTICULAR_CLOUD_SERVICES'].items():
app.lenticular_services[service_name] = model.Service.from_config(service_name, service_config)
app.pki = Pki(app.config['PKI_PATH'], app.config['DOMAIN'])
return app
def init_login_manager(app):
@app.login_manager.user_loader
def user_loader(username):
return model.User.query().by_username(username)
@app.login_manager.request_loader
def request_loader(request):
pass

View file

@ -0,0 +1,76 @@
from flask import current_app
from .form.login import PasswordForm, TotpForm, Fido2Form
from ldap3 import Server, Connection
from ldap3.core.exceptions import LDAPException
import pyotp
class AuthProvider:
@classmethod
def get_name(csl):
return csl.__name__
@staticmethod
def get_form():
return
@staticmethod
def check_auth(user, form) -> bool:
'''
checks the submited form is valid
return true if user is allowed to auth
'''
return False
class LdapAuthProvider(AuthProvider):
@staticmethod
def get_form():
return PasswordForm(prefix='password')
@staticmethod
def check_auth(user, form):
server = Server(current_app.config['LDAP_URL'])
ldap_conn = Connection(server, user.entry_dn, form.data['password'])
try:
return ldap_conn.bind()
except LDAPException:
return False
class U2FAuthProvider(AuthProvider):
@staticmethod
def get_from():
return Fido2Form(prefix='fido2')
class WebAuthProvider(AuthProvider):
pass
class TotpAuthProvider(AuthProvider):
@staticmethod
def get_form():
return TotpForm(prefix='totp')
@staticmethod
def check_auth(user, form):
data = form.data['totp']
if data is not None:
print(f'data totp: {data}')
for totp in user.totps:
if pyotp.TOTP(totp).verify(data):
return True
return False
AUTH_PROVIDER_LIST = [
LdapAuthProvider,
TotpAuthProvider
]
print(LdapAuthProvider.get_name())

View file

View file

@ -0,0 +1,24 @@
from flask_babel import gettext
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextField, \
TextAreaField, PasswordField, IntegerField, FloatField, \
DateTimeField, DateField, FormField, BooleanField, \
SelectField, Form as NoCsrfForm
from wtforms.widgets.html5 import NumberInput, DateInput
from wtforms.validators import DataRequired, NumberRange, Optional, NoneOf, Length
from datetime import datetime
class ClientCertForm(FlaskForm):
publickey = TextAreaField(gettext('Public Key'), validators=[
DataRequired()
])
valid_time = IntegerField(
gettext('valid time in days'),
default=365,
validators=[
DataRequired(),
NumberRange(min=1, max=365*2)
])
submit = SubmitField(gettext('Submit'))

View file

@ -0,0 +1,29 @@
from flask_babel import gettext
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextField, \
TextAreaField, PasswordField, IntegerField, FloatField, \
DateTimeField, DateField, FormField, BooleanField, \
SelectField, Form as NoCsrfForm
from wtforms.widgets.html5 import NumberInput, DateInput
from wtforms.validators import DataRequired, NumberRange, Optional, NoneOf, Length
from datetime import datetime
class LoginForm(FlaskForm):
name = StringField(gettext('User Name'), validators=[DataRequired()])
submit = SubmitField(gettext('Login'))
class PasswordForm(FlaskForm):
password = PasswordField(gettext('Password'))
submit = SubmitField(gettext('Authorize'))
class TotpForm(FlaskForm):
totp = TextField(gettext('2FA Token'))
submit = SubmitField(gettext('Authorize'))
class Fido2Form(FlaskForm):
fido2 = TextField(gettext('Fido2'), default="Javascript Required")
submit = SubmitField(gettext('Authorize'))

239
lenticular_cloud/model.py Normal file
View file

@ -0,0 +1,239 @@
from flask import current_app
from ldap3_orm import AttrDef, EntryBase as _EntryBase, ObjectDef, EntryType
from ldap3_orm import Reader
from ldap3 import Entry
from ldap3.utils.conv import escape_filter_chars
from flask_login import UserMixin
from ldap3.core.exceptions import LDAPSessionTerminatedByServerError
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
ldap_conn = None # type: Connection
base_dn = ''
class SecurityUser(UserMixin):
def __init__(self, username):
self._username = username
def get_id(self):
return self._username
class LambdaStr:
def __init__(self, lam):
self.lam = lam
def __str__(self):
return self.lam()
class EntryBase(object):
_type = None # will get replaced by the local type
_query_object = None # will get replaced by the local type
_base_dn = LambdaStr(lambda: base_dn)
def __init__(self, ldap_object=None, **kwargs):
if ldap_object is None:
self._ldap_object = self.get_type()(**kwargs)
else:
self._ldap_object = ldap_object
def __str__(self):
return str(self._ldap_object)
@classmethod
def get_object_def(cls):
return ObjectDef(cls.object_classes, ldap_conn)
@classmethod
def get_base(cls):
return cls.base_dn.format(_base_dn=base_dn)
@classmethod
def get_type(cls):
if cls._type is None:
cls._type = EntryType(cls.dn.replace('{base_dn}',cls.get_base()), cls.object_classes, ldap_conn)
return cls._type
def commit(self):
print(self._ldap_object.entry_attributes_as_dict)
ret = ldap_conn.add(
self.dn, self.object_classes, self._ldap_object.entry_attributes_as_dict)
print(ret)
pass
@classmethod
def query(cls):
if cls._query_object is None:
cls._query_object = cls._query(cls)
return cls._query_object
class _query(object):
def __init__(self, clazz):
self._class = clazz
def _query(self, ldap_filter: str):
reader = Reader(ldap_conn, self._class.get_object_def(), self._class.get_base(), ldap_filter)
try:
reader.search()
except LDAPSessionTerminatedByServerError:
ldap_conn.bind()
reader.search()
return list(reader)
def all(self):
return self._query(None)
class Service(object):
def __init__(self, name):
self._name = name
self._client_cert = False
self._pki_config = {
'cn': '{username}',
'email': '{username}@{domain}'
}
@staticmethod
def from_config(name, config):
"""
"""
service = Service(name)
if 'client_cert' in config:
service._client_cert = bool(config['client_cert'])
if 'pki_config' in config:
service._pki_config = config['pki_config']
return service
@property
def name(self):
return self._name
@property
def client_cert(self):
return self._client_cert
@property
def pki_config(self):
if not self._client_cert:
raise Exception('invalid call')
return self._pki_config
class Certificate(object):
def __init__(self, cn, ca_name, cert_data):
self._cn = cn
self._ca_name = ca_name
self._cert_data = cert_data
@property
def cn(self):
return self._cn
@property
def ca_name(self):
return self._ca_name
@property
def not_valid_before(self):
return self._cert_data.not_valid_before
@property
def not_valid_after(self):
return self._cert_data.not_valid_after
def fingerprint(self, algorithm=hashes.SHA256()):
return self._cert_data.fingerprint(algorithm)
def pem(self):
return self._cert_data.public_bytes(encoding=serialization.Encoding.PEM).decode()
def __str__(self):
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 User(EntryBase):
dn = "uid={uid},{base_dn}"
base_dn = "ou=users,{_base_dn}"
object_classes = ["top", "inetOrgPerson", "LenticularUser"]
@property
def is_authenticated(self):
return True # TODO
def get(self, key):
print(f'getitem: {key}')
@property
def entry_dn(self):
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
def fullname(self):
return self._ldap_object.fullname
@property
def givenname(self):
return self._ldap_object.givenname
@property
def surname(self):
return self._ldap_object.surname
@property
def mail(self):
return self._ldap_object.mail
@property
def alternative_email(self):
return self._ldap_object.altMail
@property
def auth_role(self):
return self._ldap_object.authRole
@property
def gpg_public_key(self):
return self._ldap_object.gpgPublicKey
@property
def totps(self):
return ['JBSWY3DPEHPK3PXP']
class _query(EntryBase._query):
def by_username(self, username) -> 'User':
result = self._query('(uid={username:s})'.format(username=escape_filter_chars(username)))
if len(result) > 0:
return User(result[0])
else:
return None
class Group(EntryBase):
dn = "cn={cn},{base_dn}"
base_dn = "ou=Users,{_base_dn}"
object_classes = ["top"]
fullname = AttrDef("cn")

View file

@ -0,0 +1,30 @@
from flask_sqlalchemy import SQLAlchemy, orm
db = SQLAlchemy() # type: SQLAlchemy
class Client(db.Model):
key = db.Column(db.Text, primary_key=True)
value = db.Column(db.Text)
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)

215
lenticular_cloud/pki.py Normal file
View file

@ -0,0 +1,215 @@
from flask import current_app
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID
from cryptography.x509 import ObjectIdentifier
from pathlib import Path
import os
import string
import re
import datetime
import logging
from .model import Service, User, Certificate
logger = logging.getLogger(__name__)
# partial source from https://github.com/Snawoot/quickcerts
# MIT (C) Snawoot
DAY = datetime.timedelta(1, 0, 0)
CA_FILENAME = 'ca'
KEY_EXT = 'key'
CERT_EXT = 'pem'
E = 65537
safe_symbols = set(string.ascii_letters + string.digits + '-.')
def safe_filename(name):
return "".join(c if c in safe_symbols else '_' for c in name)
class Pki(object):
def __init__(self, pki_path: str, domain: str):
'''
pki_path: str base path from the pkis
'''
self._pki_path = Path(pki_path)
self._domain = domain
def _init_ca(self, service: Service):
'''
'''
ca_name = service.name
ca_private_key = self._ensure_private_key(ca_name)
ca_cert = self._ensure_ca_cert(ca_name, ca_private_key)
pki_path = self._pki_path / ca_name
if not pki_path.exists():
pki_path.mkdir()
return (ca_private_key, ca_cert)
def get_client_certs(self, user: User, service: Service):
pki_path = self._pki_path / service.name
certs = []
for cert_path in pki_path.glob(f'{user.username}*.crt.pem'):
print(cert_path)
with cert_path.open('rb') as cert_fd:
cert_data = x509.load_pem_x509_certificate(
cert_fd.read(),
backend=default_backend())
cert = Certificate(user.username, service.name, cert_data)
certs.append(cert)
return certs
def signing_publickey(self, user: User, service: Service, publickey: str, valid_time=DAY*365):
_public_key = serialization.load_pem_public_key(
publickey.encode(), backend=default_backend())
ca_private_key, ca_cert = self._init_ca(service)
ca_name = service.name
username = str(user.username)
config = service.pki_config #TODO use this config
domain = self._domain
not_valid_before = datetime.datetime.now()
ca_public_key = ca_private_key.public_key()
end_entity_cert_builder = x509.CertificateBuilder().\
subject_name(x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, username),
x509.NameAttribute(NameOID.EMAIL_ADDRESS, f'{username}@jabber.{domain}'),
])).\
issuer_name(ca_cert.subject).\
not_valid_before(not_valid_before).\
not_valid_after(not_valid_before + valid_time).\
serial_number(x509.random_serial_number()).\
public_key(_public_key).\
add_extension(
x509.SubjectAlternativeName([
x509.DNSName(f'{username}'),
]),
critical=False).\
add_extension(
x509.BasicConstraints(ca=False, path_length=None),
critical=True).\
add_extension(
x509.KeyUsage(digital_signature=True,
content_commitment=True, # False
key_encipherment=True,
data_encipherment=False,
key_agreement=False,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False),
critical=True).\
add_extension(
x509.ExtendedKeyUsage([
ExtendedKeyUsageOID.CLIENT_AUTH,
ExtendedKeyUsageOID.SERVER_AUTH,
]), critical=False).\
add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_public_key),
critical=False).\
add_extension(
x509.SubjectKeyIdentifier.from_public_key(_public_key),
critical=False)
end_entity_cert = end_entity_cert_builder.\
sign(
private_key=ca_private_key,
algorithm=hashes.SHA256(),
backend=default_backend()
)
fingerprint =end_entity_cert.fingerprint(hashes.SHA256()).hex()
end_entity_cert_filename = self._pki_path / ca_name / \
f'{safe_filename(username)}-{fingerprint}.crt.pem'
# save cert
with end_entity_cert_filename.open("wb") as end_entity_cert_file:
end_entity_cert_file.write(
end_entity_cert.public_bytes(encoding=serialization.Encoding.PEM))
return Certificate(user.username, service.name, end_entity_cert)
def get_ca_cert_pem(self, service: Service):
ca_private_key, ca_cert = self._init_ca(service)
return ca_cert.public_bytes(encoding=serialization.Encoding.PEM).decode()
def _ensure_private_key(self, name, key_size=4096):
key_filename = self._pki_path / f'{safe_filename(name)}.key.pem'
if key_filename.exists():
with open(key_filename, "rb") as key_file:
private_key = serialization.load_pem_private_key(key_file.read(),
password=None, backend=default_backend())
else:
logger.info(f'Generate new Private key for {name}')
private_key = rsa.generate_private_key(public_exponent=E,
key_size=key_size, backend=default_backend())
with key_filename.open('wb') as key_file:
key_file.write(private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()))
return private_key
def _ensure_ca_cert(self, ca_name, ca_private_key):
ca_cert_filename =self._pki_path / f'{ca_name}.crt.pem'
ca_public_key = ca_private_key.public_key()
if ca_cert_filename.exists():
with ca_cert_filename.open("rb") as ca_cert_file:
ca_cert = x509.load_pem_x509_certificate(
ca_cert_file.read(),
backend=default_backend())
else:
logger.info(f'Generate new Certificate key for {ca_name}')
iname = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, f'Lenticular Cloud CA - {ca_name}'),
x509.NameAttribute(NameOID.ORGANIZATION_NAME,
'Lenticluar Cloud'),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME,
ca_name),
])
ca_cert = x509.CertificateBuilder().\
subject_name(iname).\
issuer_name(iname).\
not_valid_before(datetime.datetime.today() - DAY).\
not_valid_after(datetime.datetime.today() + 3650 * DAY).\
serial_number(x509.random_serial_number()).\
public_key(ca_public_key).\
add_extension(
x509.BasicConstraints(ca=True, path_length=None),
critical=True).\
add_extension(
x509.KeyUsage(digital_signature=False,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=True,
crl_sign=True,
encipher_only=False,
decipher_only=False),
critical=True).\
add_extension(
x509.SubjectKeyIdentifier.from_public_key(ca_public_key),
critical=False).\
sign(
private_key=ca_private_key,
algorithm=hashes.SHA256(),
backend=default_backend()
)
with open(ca_cert_filename, "wb") as ca_cert_file:
ca_cert_file.write(
ca_cert.public_bytes(encoding=serialization.Encoding.PEM))
assert isinstance(ca_cert, x509.Certificate)
return ca_cert

View file

@ -0,0 +1,48 @@
from flask import g, request, Flask
from flask_login import current_user
from typing import Optional
LANGUAGES = {
'en': 'English',
'de': 'Deutsch'
}
def init_babel(app: Flask) -> None:
babel = app.babel
@babel.localeselector
def get_locale() -> str:
# if a user is logged in, use the locale from the user settings
user = current_user # type: Optional[User]
return 'de'
# prefer lang argument
if 'lang' in request.args:
lang = request.args['lang'] # type: str
if lang in LANGUAGES:
if not isinstance(user, User):
return lang
user.locale = lang
db.session.commit()
if isinstance(user, User):
return user.locale
# otherwise try to guess the language from the user accept
# header the browser transmits. We support de/fr/en in this
# example. The best match wins.
return request.accept_languages.best_match(['de'])
@babel.timezoneselector
def get_timezone() -> Optional[str]:
# user = getattr(g, 'user', None)
# if user is not None:
# return user.timezone
return None
@app.context_processor
def get_locale_jinja() -> dict:
def get_locale_() -> str:
return get_locale()
return dict(get_locale=get_locale_)

View file

View file

@ -0,0 +1,5 @@
# pylint: disable=unused-import
from .oidc import oidc_provider_views
from .auth import auth_views
from .frontend import frontend_views

View file

@ -0,0 +1,73 @@
from urllib.parse import urlencode, parse_qs
import flask
from flask import Blueprint, redirect
from flask import current_app, session
from flask import jsonify
from flask.helpers import make_response
from flask.templating import render_template
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
from flask_login import login_required, login_user, logout_user
from werkzeug.utils import redirect
import logging
from ..model import User, SecurityUser
from ..form.login import LoginForm
from ..auth_providers import AUTH_PROVIDER_LIST
from .oidc import do_logout
auth_views = Blueprint('auth', __name__, url_prefix='')
@auth_views.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query().by_username(form.data['name'])
session['username'] = str(user.username)
session['auth_providers'] = []
return redirect(url_for('auth.login_auth'))
return render_template('frontend/login.html.j2', form=form)
@auth_views.route('/login/auth', methods=['GET', 'POST'])
def login_auth():
if 'username' not in session:
return redirect(url_for('auth.login'))
auth_forms = []
user = User.query().by_username(session['username'])
for auth_provider in AUTH_PROVIDER_LIST:
form = auth_provider.get_form()
if auth_provider.get_name() not in session['auth_providers'] and\
auth_provider.check_auth(user, form):
session['auth_providers'].append(auth_provider.get_name())
if auth_provider.get_name() not in session['auth_providers']:
auth_forms.append(form)
if len(session['auth_providers']) >= 2:
login_user(SecurityUser(session['username']))
# TODO use this var
_next = request.args.get('next')
return redirect(url_for('frontend.index'))
print(auth_forms)
return render_template('frontend/login_auth.html.j2', forms=auth_forms)
@auth_views.route("/logout")
@login_required
def logout():
logout_user()
do_logout()
return redirect(url_for('.login'))

View file

@ -0,0 +1,93 @@
from urllib.parse import urlencode, parse_qs
import flask
from flask import Blueprint, redirect
from flask import current_app, session
from flask import jsonify, send_file
from flask.helpers import make_response
from flask.templating import render_template
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
from flask_login import login_required, login_user, logout_user, current_user
from werkzeug.utils import redirect
import logging
from datetime import timedelta
from ..model import User, SecurityUser
from ..form.login import LoginForm
from ..form.frontend import ClientCertForm
from ..auth_providers import AUTH_PROVIDER_LIST
frontend_views = Blueprint('frontend', __name__, url_prefix='')
@frontend_views.route('/', methods=['GET'])
@login_required
def index():
return render_template('frontend/index.html.j2')
@frontend_views.route('/client_cert')
@login_required
def client_cert():
client_certs = {}
for service in current_app.lenticular_services.values():
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)
@frontend_views.route('/client_cert/<service_name>/<fingerprint>')
@login_required
def get_client_cert(service_name, fingerprint):
service = current_app.lenticular_services[service_name]
current_app.pki.get_client_cert(current_user, service, fingerprint)
pass
@frontend_views.route(
'/client_cert/<service_name>/new',
methods=['GET', 'POST'])
@login_required
def client_cert_new(service_name):
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)
return jsonify( {
'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
})
return render_template('frontend/client_cert_new.html.j2',
service=service,
form=form)
@frontend_views.route('/totp')
@login_required
def totp():
return render_template('frontend/totp.html.j2')

View file

@ -0,0 +1,113 @@
from urllib.parse import urlencode, parse_qs
import flask
from flask import Blueprint, redirect
from flask import current_app, session
from flask import jsonify
from flask.helpers import make_response
from flask.templating import render_template
from oic.oic.message import TokenErrorResponse, UserInfoErrorResponse, EndSessionRequest
from flask_login import current_user, login_required
from pyop.access_token import AccessToken, BearerTokenError
from pyop.exceptions import InvalidAuthenticationRequest, InvalidAccessToken, InvalidClientAuthentication, OAuthError, \
InvalidSubjectIdentifier, InvalidClientRegistrationRequest
from pyop.util import should_fragment_encode
oidc_provider_views = Blueprint('oidc_provider', __name__, url_prefix='')
@oidc_provider_views.route('/registration', methods=['POST'])
def registration_endpoint():
try:
response = current_app.provider.handle_client_registration_request(flask.request.get_data().decode('utf-8'))
return make_response(jsonify(response.to_dict()), 201)
except InvalidClientRegistrationRequest as e:
print(e)
return make_response(jsonify(str(e)), 400)
@oidc_provider_views.route('/authentication', methods=['GET'])
@login_required
def authentication_endpoint():
# parse authentication request
print(flask.request)
print(flask.request.headers)
try:
auth_req = current_app.provider.parse_authentication_request(urlencode(flask.request.args),
flask.request.args)
except InvalidAuthenticationRequest as e:
current_app.logger.debug('received invalid authn request', exc_info=True)
error_url = e.to_error_url()
if error_url:
return redirect(error_url, 303)
else:
# show error to user
return make_response('Something went wrong: {}'.format(str(e)), 400)
# automagic authentication
authn_response = current_app.provider.authorize(auth_req, str(current_user.username))
response_url = authn_response.request(auth_req['redirect_uri'], should_fragment_encode(auth_req))
return redirect(response_url, 303)
@oidc_provider_views.route('/.well-known/openid-configuration')
def provider_configuration():
return jsonify(current_app.provider.provider_configuration.to_dict())
@oidc_provider_views.route('/jwks')
def jwks_uri():
return jsonify(current_app.provider.jwks)
@oidc_provider_views.route('/token', methods=['POST'])
def token_endpoint():
try:
token_response = current_app.provider.handle_token_request(flask.request.get_data().decode('utf-8'),
flask.request.headers)
return jsonify(token_response.to_dict())
except InvalidClientAuthentication as e:
current_app.logger.debug('invalid client authentication at token endpoint', exc_info=True)
error_resp = TokenErrorResponse(error='invalid_client', error_description=str(e))
response = make_response(error_resp.to_json(), 401)
response.headers['Content-Type'] = 'application/json'
response.headers['WWW-Authenticate'] = 'Basic'
return response
except OAuthError as e:
current_app.logger.debug('invalid request: %s', str(e), exc_info=True)
error_resp = TokenErrorResponse(error=e.oauth_error, error_description=str(e))
response = make_response(error_resp.to_json(), 400)
response.headers['Content-Type'] = 'application/json'
return response
@oidc_provider_views.route('/userinfo', methods=['GET', 'POST'])
def userinfo_endpoint():
try:
response = current_app.provider.handle_userinfo_request(flask.request.get_data().decode('utf-8'),
flask.request.headers)
return jsonify(response.to_dict())
except (BearerTokenError, InvalidAccessToken) as e:
error_resp = UserInfoErrorResponse(error='invalid_token', error_description=str(e))
response = make_response(error_resp.to_json(), 401)
response.headers['WWW-Authenticate'] = AccessToken.BEARER_TOKEN_TYPE
response.headers['Content-Type'] = 'application/json'
return response
def do_logout():
end_session_request = EndSessionRequest().deserialize(urlencode(flask.request.args)).to_dict()
try:
current_app.provider.logout_user(end_session_request=end_session_request)
except InvalidSubjectIdentifier as e:
return make_response('Logout unsuccessful!', 400)
redirect_url = current_app.provider.do_post_logout_redirect(end_session_request)
if redirect_url:
return redirect(redirect_url, 303)
return make_response('Logout successful!')