added a lot of auth code

This commit is contained in:
2019-03-21 16:17:25 +01:00
parent bef3c6dc9b
commit 0469b8dbb5
13 changed files with 220 additions and 23 deletions

View File

@@ -7,20 +7,23 @@ from flask import Flask
from flask_httpauth import HTTPTokenAuth, HTTPBasicAuth, MultiAuth from flask_httpauth import HTTPTokenAuth, HTTPBasicAuth, MultiAuth
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
jwt_auth = HTTPTokenAuth()
basic_auth = HTTPBasicAuth()
multi_auth = MultiAuth(basic_auth, jwt_auth)
app = Flask(__name__) app = Flask(__name__)
app.config.from_object('backend.config.Config') app.config.from_object('backend.config.Config')
db = SQLAlchemy(app) 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 .serve_frontend import fe_bp
from .api import auth_api_bp, api_bp from .api import auth_api_bp, api_bp
from backend.auth import oidc_auth app.register_blueprint(auth_bp)
oidc_auth.init_app(app)
app.register_blueprint(auth_api_bp) app.register_blueprint(auth_api_bp)
app.register_blueprint(api_bp) app.register_blueprint(api_bp)
app.register_blueprint(fe_bp) app.register_blueprint(fe_bp)

View File

@@ -1,15 +1,39 @@
# Copyright (c) 2019. Tobias Kurze # 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 import jwt
from flask import request, jsonify, current_app from flask import request, jsonify, current_app, url_for
from functools import wraps from functools import wraps
from random import randint from random import randint
from flask_login import logout_user, login_user
from backend import db from backend import db
from backend.api import auth_api_bp from backend.api import auth_api_bp
from backend.auth import AUTH_PROVIDERS
from backend.models.user_model import User 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',)) @auth_api_bp.route('/register', methods=('POST',))
def register(): def register():
data = request.get_json() data = request.get_json()
@@ -21,15 +45,21 @@ def register():
@auth_api_bp.route('/login', methods=('GET', 'POST',)) @auth_api_bp.route('/login', methods=('GET', 'POST',))
def login(): def login():
print("login")
print(request)
data = request.get_json() data = request.get_json()
print(data)
user = User.authenticate(**data) user = User.authenticate(**data)
if not user: if not user:
return jsonify({ 'message': 'Invalid credentials', 'authenticated': False }), 401 return jsonify({'message': 'Invalid credentials', 'authenticated': False}), 401
token = jwt.encode({ token = create_jwt(user)
'sub': user.email, #login_user(user)
'iat':datetime.utcnow(),
'exp': datetime.utcnow() + datetime.timedelta(minutes=30)},
current_app.config['SECRET_KEY'])
return jsonify({'token': token.decode('UTF-8')}) return jsonify({'token': token.decode('UTF-8')})
@auth_api_bp.route('/logout', methods=('GET', ))
def logout():
pass
#logout_user()

View File

@@ -6,7 +6,7 @@ from random import *
from flask import jsonify, Blueprint, request from flask import jsonify, Blueprint, request
from flask_restplus import Resource, reqparse 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 from backend.api import api_v1, api_bp
@@ -21,6 +21,18 @@ def random_number():
return jsonify(response) 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): class HelloWorld(Resource):
""" """
This is a test class. This is a test class.

View File

@@ -1,6 +1,44 @@
# Copyright (c) 2019. Tobias Kurze # 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 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)

14
auth/basic_auth.py Normal file
View File

@@ -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)

17
auth/config.py Normal file
View File

@@ -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"

View File

@@ -1,6 +1,15 @@
# Copyright (c) 2019. Tobias Kurze # Copyright (c) 2019. Tobias Kurze
"""
OIDC login auth module
"""
import flask
from flask import jsonify
from flask_pyoidc.flask_pyoidc import OIDCAuthentication 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): 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_orig = OIDCAuthentication.oidc_auth
OIDCAuthentication.oidc_auth = oidc_auth_default_provider 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'])

View File

@@ -11,4 +11,4 @@ PROVIDER_NAME = 'kit_oidc'
PROVIDER_CONFIG = ProviderConfiguration(issuer=PROVIDER_URL, PROVIDER_CONFIG = ProviderConfiguration(issuer=PROVIDER_URL,
client_metadata=CLIENT_METADATA) client_metadata=CLIENT_METADATA)
PROVIDERS = {PROVIDER_NAME: PROVIDER_CONFIG} OIDC_PROVIDERS = {PROVIDER_NAME: PROVIDER_CONFIG}

23
auth/templates/login.html Normal file
View File

@@ -0,0 +1,23 @@
<html>
<head>
<title>Flask Intro - login page</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="static/bootstrap.min.css" rel="stylesheet" media="screen">
</head>
<body>
<div class="container">
<h1>Please login</h1>
<br>
<form action="" method="post">
<input type="text" placeholder="Username" name="username" value="{{
request.form.username }}">
<input type="password" placeholder="Password" name="password" value="{{
request.form.password }}">
<input class="btn btn-default" type="submit" value="Login">
</form>
{% if error %}
<p class="error"><strong>Error:</strong> {{ error }}
{% endif %}
</div>
</body>
</html>

View File

@@ -0,0 +1,21 @@
<html>
<head>
<title>Flask Intro - login page</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="static/bootstrap.min.css" rel="stylesheet" media="screen">
</head>
<body>
<div class="container">
<h1>Please select login method</h1>
<br>
<ul>
{% for provider in providers %}
<li><a href="{{url_for(providers[provider].url)}}">{{ provider }} ({{ providers[provider].type }})</a></li>
{% endfor %}
</ul>
{% if error %}
<p class="error"><strong>Error:</strong> {{ error }}
{% endif %}
</div>
</body>
</html>

View File

@@ -6,10 +6,13 @@ basedir = os.path.abspath(os.path.dirname(__file__))
class Config(): 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.dev"
#SERVER_NAME = "localhost:5443" SERVER_NAME = "localhost:5443"
PREFERRED_URL_SCHEME = 'https' #SERVER_NAME = "localhost"
#SERVER_NAME = "localhost.localdomain"
#PORT = 5443
#PREFERRED_URL_SCHEME = 'https'
TEMPLATE_AUTO_RELOAD = True TEMPLATE_AUTO_RELOAD = True

View File

@@ -102,6 +102,20 @@ class User(UserMixin, db.Model):
""" """
return re.sub('[^a-zA-Z0-9_.]', '', nickname) 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 @property
def is_authenticated(self): def is_authenticated(self):
""" """

View File

@@ -43,6 +43,7 @@ def test_oidc():
token_header=token_header) token_header=token_header)
def has_no_empty_params(rule): def has_no_empty_params(rule):
defaults = rule.defaults if rule.defaults is not None else () defaults = rule.defaults if rule.defaults is not None else ()
arguments = rule.arguments if rule.arguments is not None else () arguments = rule.arguments if rule.arguments is not None else ()