From 0469b8dbb58f68f9564beab7452cde3cd12a4563 Mon Sep 17 00:00:00 2001 From: Tobias Date: Thu, 21 Mar 2019 16:17:25 +0100 Subject: [PATCH] added a lot of auth code --- __init__.py | 17 +++++++----- api/auth_api.py | 46 ++++++++++++++++++++++++++------ api/example_api.py | 14 +++++++++- auth/__init__.py | 42 +++++++++++++++++++++++++++-- auth/basic_auth.py | 14 ++++++++++ auth/config.py | 17 ++++++++++++ auth/oidc.py | 23 +++++++++++++++- auth/oidc_config.py | 2 +- auth/templates/login.html | 23 ++++++++++++++++ auth/templates/login_select.html | 21 +++++++++++++++ config.py | 9 ++++--- models/user_model.py | 14 ++++++++++ serve_frontend.py | 1 + 13 files changed, 220 insertions(+), 23 deletions(-) create mode 100644 auth/basic_auth.py create mode 100644 auth/config.py create mode 100644 auth/templates/login.html create mode 100644 auth/templates/login_select.html diff --git a/__init__.py b/__init__.py index 7f990b6..7826906 100644 --- a/__init__.py +++ b/__init__.py @@ -7,20 +7,23 @@ from flask import Flask from flask_httpauth import HTTPTokenAuth, HTTPBasicAuth, MultiAuth from flask_sqlalchemy import SQLAlchemy -jwt_auth = HTTPTokenAuth() -basic_auth = HTTPBasicAuth() -multi_auth = MultiAuth(basic_auth, jwt_auth) - app = Flask(__name__) app.config.from_object('backend.config.Config') db = SQLAlchemy(app) +jwt_auth = HTTPTokenAuth() +basic_auth = HTTPBasicAuth() +multi_auth = MultiAuth(basic_auth, jwt_auth) +from backend.auth import oidc_auth, auth_bp + +oidc_auth.init_app(app) + +# oidc_multi_auth = MultiAuth(oidc_auth, jwt_auth) <- can't work as OIDCAuthentication not implementing HTTPAuth + from .serve_frontend import fe_bp from .api import auth_api_bp, api_bp -from backend.auth import oidc_auth -oidc_auth.init_app(app) - +app.register_blueprint(auth_bp) app.register_blueprint(auth_api_bp) app.register_blueprint(api_bp) app.register_blueprint(fe_bp) diff --git a/api/auth_api.py b/api/auth_api.py index 9739d2d..1e67bf9 100644 --- a/api/auth_api.py +++ b/api/auth_api.py @@ -1,15 +1,39 @@ # Copyright (c) 2019. Tobias Kurze -import datetime +""" +This module provides functions related to authentication through the API. +For example: listing of available auth providers or registration of users. +""" +from datetime import datetime, timedelta import jwt -from flask import request, jsonify, current_app +from flask import request, jsonify, current_app, url_for from functools import wraps from random import randint +from flask_login import logout_user, login_user + from backend import db from backend.api import auth_api_bp +from backend.auth import AUTH_PROVIDERS from backend.models.user_model import User +def create_jwt(user: User, validity_min=30): + return jwt.encode({ + 'sub': user.email, + 'iat': datetime.utcnow(), + 'exp': datetime.utcnow() + timedelta(minutes=validity_min)}, + current_app.config['SECRET_KEY']) + + +@auth_api_bp.route('/providers', methods=('GET',)) +def get_auth_providers(): + providers = list() + for p in AUTH_PROVIDERS: + provider = dict(p) + provider["url"] = url_for(p["url"]) + return jsonify(providers) + + @auth_api_bp.route('/register', methods=('POST',)) def register(): data = request.get_json() @@ -21,15 +45,21 @@ def register(): @auth_api_bp.route('/login', methods=('GET', 'POST',)) def login(): + print("login") + print(request) data = request.get_json() + print(data) user = User.authenticate(**data) if not user: - return jsonify({ 'message': 'Invalid credentials', 'authenticated': False }), 401 + return jsonify({'message': 'Invalid credentials', 'authenticated': False}), 401 - token = jwt.encode({ - 'sub': user.email, - 'iat':datetime.utcnow(), - 'exp': datetime.utcnow() + datetime.timedelta(minutes=30)}, - current_app.config['SECRET_KEY']) + token = create_jwt(user) + #login_user(user) return jsonify({'token': token.decode('UTF-8')}) + + +@auth_api_bp.route('/logout', methods=('GET', )) +def logout(): + pass + #logout_user() diff --git a/api/example_api.py b/api/example_api.py index d686586..1e00900 100644 --- a/api/example_api.py +++ b/api/example_api.py @@ -6,7 +6,7 @@ from random import * from flask import jsonify, Blueprint, request from flask_restplus import Resource, reqparse -from backend import basic_auth, multi_auth, db +from backend import basic_auth, multi_auth, db, jwt_auth from backend.api import api_v1, api_bp @@ -21,6 +21,18 @@ def random_number(): return jsonify(response) +@api_bp.route('/test_jwt') +@jwt_auth.login_required +def random_number_jwt(): + """ + :return: a random number + """ + response = { + 'randomNumber': randint(1, 100) + } + return jsonify(response) + + class HelloWorld(Resource): """ This is a test class. diff --git a/auth/__init__.py b/auth/__init__.py index cb976d9..bc23f75 100644 --- a/auth/__init__.py +++ b/auth/__init__.py @@ -1,6 +1,44 @@ # Copyright (c) 2019. Tobias Kurze +""" +Base module for auth aspects. + +Also this module contains mainly code for login through HTML pages served by the backend. +If frontend pages are build by frontend code (JS, etc.) authentication should consider using api functions. +(For more info, see api.auth_api.py.) +""" +from flask import Blueprint +from werkzeug.routing import BuildError + +auth_bp = Blueprint('auth', __name__, url_prefix='/auth', template_folder='templates') + +from backend.auth.config import AUTH_PROVIDERS, DEFAULT_PROVIDER from backend.auth.oidc import OIDCAuthentication +from backend.auth.oidc_config import OIDC_PROVIDERS -from backend.auth.oidc_config import PROVIDERS +oidc_auth = OIDCAuthentication(OIDC_PROVIDERS) -oidc_auth = OIDCAuthentication(PROVIDERS) +from .basic_auth import * + + +def auth_decorator(): # custom decorator + pass + + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + try: + prov = AUTH_PROVIDERS[DEFAULT_PROVIDER] + except KeyError: + return "No known default provider specified!" + url = prov["url"] + try: + url = url_for(prov["url"], next=request.endpoint) + except BuildError as e: + pass + #logger.log("Can't create endpoint for '{}' (specified provider: {}).".format(e.endpoint, DEFAULT_PROVIDER)) + return redirect(url) + + +@auth_bp.route('/login_select', methods=['GET']) +def login_select(): + return render_template('login_select.html', providers=AUTH_PROVIDERS) diff --git a/auth/basic_auth.py b/auth/basic_auth.py new file mode 100644 index 0000000..e91fa04 --- /dev/null +++ b/auth/basic_auth.py @@ -0,0 +1,14 @@ +# Route for handling the login page logic +from flask import request, redirect, render_template, url_for +from backend.auth import auth_bp + + +@auth_bp.route('/base_login', methods=['GET', 'POST']) +def base_login(): + error = None + if request.method == 'POST': + if request.form['username'] != 'admin' or request.form['password'] != 'admin': + error = 'Invalid Credentials. Please try again.' + else: + return redirect("/") + return render_template('login.html', error=error) diff --git a/auth/config.py b/auth/config.py new file mode 100644 index 0000000..471b8b0 --- /dev/null +++ b/auth/config.py @@ -0,0 +1,17 @@ +from typing import Dict, List + +AUTH_PROVIDERS: Dict[str, Dict[str, str]] = { + "KIT OIDC": + { + "type": "oidc", + "url": "auth.oidc" + }, + "Base Login": + { + "type": "login_form", + "url": "auth.base_login" + }, +} + +DEFAULT_PROVIDER: str = "Base Login" +#DEFAULT_PROVIDER: str = "KIT OIDC" diff --git a/auth/oidc.py b/auth/oidc.py index 3e0d174..9931154 100644 --- a/auth/oidc.py +++ b/auth/oidc.py @@ -1,6 +1,15 @@ # Copyright (c) 2019. Tobias Kurze +""" +OIDC login auth module +""" + +import flask +from flask import jsonify from flask_pyoidc.flask_pyoidc import OIDCAuthentication -from backend.auth.oidc_config import PROVIDER_NAME +from flask_pyoidc.user_session import UserSession + +from .import auth_bp +from .oidc_config import PROVIDER_NAME, OIDC_PROVIDERS def oidc_auth_default_provider(self): @@ -9,3 +18,15 @@ def oidc_auth_default_provider(self): OIDCAuthentication.oidc_auth_orig = OIDCAuthentication.oidc_auth OIDCAuthentication.oidc_auth = oidc_auth_default_provider + +oidc_auth = OIDCAuthentication(OIDC_PROVIDERS) + +@auth_bp.route('/oidc', methods=['GET', 'POST']) +@oidc_auth.oidc_auth() +def oidc(): + pass + user_session = UserSession(flask.session) + access_token = user_session.access_token + + #login_user(user) + return jsonify(id_token=flask.session['id_token'], access_token=flask.session['access_token']) \ No newline at end of file diff --git a/auth/oidc_config.py b/auth/oidc_config.py index f59fb6b..4361f45 100644 --- a/auth/oidc_config.py +++ b/auth/oidc_config.py @@ -11,4 +11,4 @@ PROVIDER_NAME = 'kit_oidc' PROVIDER_CONFIG = ProviderConfiguration(issuer=PROVIDER_URL, client_metadata=CLIENT_METADATA) -PROVIDERS = {PROVIDER_NAME: PROVIDER_CONFIG} +OIDC_PROVIDERS = {PROVIDER_NAME: PROVIDER_CONFIG} diff --git a/auth/templates/login.html b/auth/templates/login.html new file mode 100644 index 0000000..08bc1e8 --- /dev/null +++ b/auth/templates/login.html @@ -0,0 +1,23 @@ + + + Flask Intro - login page + + + + +
+

Please login

+
+
+ + + +
+ {% if error %} +

Error: {{ error }} + {% endif %} +

+ + diff --git a/auth/templates/login_select.html b/auth/templates/login_select.html new file mode 100644 index 0000000..fef03c6 --- /dev/null +++ b/auth/templates/login_select.html @@ -0,0 +1,21 @@ + + + Flask Intro - login page + + + + +
+

Please select login method

+
+ + {% if error %} +

Error: {{ error }} + {% endif %} +

+ + diff --git a/config.py b/config.py index 536730c..1646ee1 100644 --- a/config.py +++ b/config.py @@ -6,10 +6,13 @@ basedir = os.path.abspath(os.path.dirname(__file__)) class Config(): - SERVER_NAME = "ubkaps154.ubka.uni-karlsruhe.de:5443" + #SERVER_NAME = "ubkaps154.ubka.uni-karlsruhe.de:5443" #SERVER_NAME = "localhost.dev" - #SERVER_NAME = "localhost:5443" - PREFERRED_URL_SCHEME = 'https' + SERVER_NAME = "localhost:5443" + #SERVER_NAME = "localhost" + #SERVER_NAME = "localhost.localdomain" + #PORT = 5443 + #PREFERRED_URL_SCHEME = 'https' TEMPLATE_AUTO_RELOAD = True diff --git a/models/user_model.py b/models/user_model.py index 9e88582..035a85c 100644 --- a/models/user_model.py +++ b/models/user_model.py @@ -102,6 +102,20 @@ class User(UserMixin, db.Model): """ return re.sub('[^a-zA-Z0-9_.]', '', nickname) + @classmethod + def authenticate(cls, **kwargs): + email = kwargs.get('email') + password = kwargs.get('password') + + if not email or not password: + return None + + user = cls.query.filter_by(email=email).first() + if not user or not user.verify_password(password): + return None + + return user + @property def is_authenticated(self): """ diff --git a/serve_frontend.py b/serve_frontend.py index 5cb8d05..0728456 100644 --- a/serve_frontend.py +++ b/serve_frontend.py @@ -43,6 +43,7 @@ def test_oidc(): token_header=token_header) + def has_no_empty_params(rule): defaults = rule.defaults if rule.defaults is not None else () arguments = rule.arguments if rule.arguments is not None else ()