added user and group API and models
This commit is contained in:
1
Pipfile
1
Pipfile
@@ -19,6 +19,7 @@ coverage = "*"
|
|||||||
flask-testing = "*"
|
flask-testing = "*"
|
||||||
flask-pyoidc = "*"
|
flask-pyoidc = "*"
|
||||||
python-jose = "*"
|
python-jose = "*"
|
||||||
|
flask-jwt-extended = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
|
|||||||
93
Pipfile.lock
generated
93
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "e688fa75b0dde0fed147314666df07f60d9a4abf5fb8646ce3f2b85adf744a36"
|
"sha256": "bde392334efe0c90dfcb180d0fc07549827e819343af266b4fa43be0bc1c0596"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -38,10 +38,10 @@
|
|||||||
},
|
},
|
||||||
"apispec": {
|
"apispec": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:9300142aa93e0c020e6b223a196cd2103ac4a61bcceea7dba894c0959b72e327",
|
"sha256:6746a57f1395fc201d06b051e43e8a7fdd23138607ebbeabdfcc6477bc3cc956",
|
||||||
"sha256:bcfe21887ba7c6e94c4be00f10564478a0d9109bb8e574aae97442909fd69b31"
|
"sha256:f0a5ddaee255eebeda8e84659028c3523b7801c7023f9364972035d36b23d734"
|
||||||
],
|
],
|
||||||
"version": "==1.1.0"
|
"version": "==1.1.1"
|
||||||
},
|
},
|
||||||
"asn1crypto": {
|
"asn1crypto": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -208,6 +208,13 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.2.4"
|
"version": "==3.2.4"
|
||||||
},
|
},
|
||||||
|
"flask-jwt-extended": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:68035dba637fd58c0f0b308e49103cba7f6977aa301d958ddbd7b811a91c6dec"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.18.0"
|
||||||
|
},
|
||||||
"flask-login": {
|
"flask-login": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec"
|
"sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec"
|
||||||
@@ -309,9 +316,9 @@
|
|||||||
},
|
},
|
||||||
"mako": {
|
"mako": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae"
|
"sha256:04092940c0df49b01f43daea4f5adcecd0e50ef6a4b222be5ac003d5d84b2843"
|
||||||
],
|
],
|
||||||
"version": "==1.0.7"
|
"version": "==1.0.8"
|
||||||
},
|
},
|
||||||
"markupsafe": {
|
"markupsafe": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -348,10 +355,10 @@
|
|||||||
},
|
},
|
||||||
"marshmallow": {
|
"marshmallow": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:01412e979b45c003aeb3632718780b15b01566ae0182cc9232434b30f6b85e1b",
|
"sha256:0e497a6447ffaad55578138ca512752de7a48d12f444996ededc3d6bf8a09ca2",
|
||||||
"sha256:8a1a2e13c6a621f4970faf21e5d9b146e451e779d0f334a96eae4fcdef53455f"
|
"sha256:e21a4dea20deb167c723e0ffb13f4cf33bcbbeb8a334e92406a3308cedea2826"
|
||||||
],
|
],
|
||||||
"version": "==2.19.1"
|
"version": "==2.19.2"
|
||||||
},
|
},
|
||||||
"oic": {
|
"oic": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -389,36 +396,36 @@
|
|||||||
},
|
},
|
||||||
"pycryptodomex": {
|
"pycryptodomex": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0bda549e20db1eb8e29fb365d10acf84b224d813b1131c828fc830b2ce313dcd",
|
"sha256:00e0a7c992756c8d6d63f2cf992276ca62e702629bfcb54f57a192624c22c3d9",
|
||||||
"sha256:1210c0818e5334237b16d99b5785aa0cee815d9997ee258bd5e2936af8e8aa50",
|
"sha256:04689a34f5cf54bd049371c6b16ca44ed4c4202f3f9d58ff25e75016808d076e",
|
||||||
"sha256:2090dc8cd7843eae75bd504b9be86792baa171fc5a758ea3f60188ab67ca95cf",
|
"sha256:088689b91b8dc0df52710e88ef447c6ca087c1f82398b8a41537c2a907f03e6a",
|
||||||
"sha256:22e6784b65dfdd357bf9a8a842db445192b227103e2c3137a28c489c46742135",
|
"sha256:1607d36788ff97b43abb896f7f81a2eed1bdc86e982e953e36b9afeb01942c9c",
|
||||||
"sha256:2edb8c3965a77e3092b5c5c1233ffd32de083f335202013f52d662404191ac79",
|
"sha256:184356bb5039fc24c337dbe485b406425d6f0ee2659d59c313e69703063bf518",
|
||||||
"sha256:310fe269ac870135ff610d272e88dcb594ee58f40ac237a688d7c972cbca43e8",
|
"sha256:21cde61813059437206f745b620068d22cfe5059134eefe858ba7e4b639c410f",
|
||||||
"sha256:456136b7d459f000794a67b23558351c72e21f0c2d4fcaa09fc99dae7844b0ef",
|
"sha256:27a7968b231c1dec2a3d4cc66621c97ee623abda65fc1df392aa19a764f2e803",
|
||||||
"sha256:463e49a9c5f1fa7bd36aff8debae0b5c487868c1fb66704529f2ad7e92f0cc9f",
|
"sha256:42c089411f55663c08e04c5bb8ce51d18002534ae76e8d2e1dd560b3d44dec5c",
|
||||||
"sha256:4a33b2828799ef8be789a462e6645ea6fe2c42b0df03e6763ccbfd1789c453e6",
|
"sha256:42e20e201267726c2d0687886d08f3e97192e0fa456535bb62274dd4b190dc0a",
|
||||||
"sha256:5ff02dff1b03929e6339226b318aa59bd0b5c362f96e3e0eb7f3401d30594ed3",
|
"sha256:47ac3a9e87e559d1bfd60ebb5a81eac66f080ec5593c77d9e33b811bee929c87",
|
||||||
"sha256:6b1db8234b8ee2b30435d9e991389c2eeae4d45e09e471ffe757ba1dfae682bb",
|
"sha256:73f72f7670d2182077fc36420e651ddc6eaa7c8478bf586719f051ecafec533e",
|
||||||
"sha256:6eb67ee02de143cd19e36a52bd3869a9dc53e9184cd6bed5c39ff71dee2f6a45",
|
"sha256:84182dd94089287269a93f4d49226915b3aab386cba587dd8ec68da01bfa3e25",
|
||||||
"sha256:6f42eea5afc7eee29494fdfddc6bb7173953d4197d9200e4f67096c2a24bc21b",
|
"sha256:898c311e6fc8df2c4aba67c554276e5e798f8fb65d1854e26494f3d809314b91",
|
||||||
"sha256:87bc8082e2de2247df7d0b161234f8edb1384294362cc0c8db9324463097578b",
|
"sha256:96352d121e5a750b006e2a357d0a9913f12c6652ca07a5e61c74f63d8be0394f",
|
||||||
"sha256:8df93d34bc0e3a28a27652070164683a07d8a50c628119d6e0f7710f4d01b42f",
|
"sha256:9a18a7078cfdf61e020f0705c6b7188513973af68a26df42b7aaaa395b5e3e2f",
|
||||||
"sha256:989952c39e8fef1c959f0a0f85656e29c41c01162e33a3f5fd8ce71e47262ae9",
|
"sha256:9ecce5ab26737dde0beeff894344d56758e0711f2883619f10625d6a46d8116c",
|
||||||
"sha256:a4a203077e2f312ec8677dde80a5c4e6fe5a82a46173a8edc8da668602a3e073",
|
"sha256:a4bac3507bcefd43e26a92182bcde27c4e37dbe4385b12ba04025a8bf93be5e7",
|
||||||
"sha256:a793c1242dffd39f585ae356344e8935d30f01f6be7d4c62ffc87af376a2f5f9",
|
"sha256:ae044ff318656aaacdfce4d91c369ead871d170d03dfd44ee37e46734f06d24e",
|
||||||
"sha256:b70fe991564e178af02ccf89435a8f9e8d052707a7c4b95bf6027cb785da3175",
|
"sha256:c8186e82d2854738ae7c18c9683bf023aa13d564b5c845a4cfd83063cf44e182",
|
||||||
"sha256:b83594196e3661cb78c97b80a62fbfbba2add459dfd532b58e7a7c62dd06aab4",
|
"sha256:d2bc76e6edbcc8b32465c7e47f045398bb857390aca0782bfc6985919cf9d27c",
|
||||||
"sha256:ba27725237d0a3ea66ec2b6b387259471840908836711a3b215160808dffed0f",
|
"sha256:d89db6097c644f814c9f8995a6a6e4b13dffcc82dcdb6ca06dc8a12545d36319",
|
||||||
"sha256:d1ab8ad1113cdc553ca50c4d5f0142198c317497364c0c70443d69f7ad1c9288",
|
"sha256:deb56cc3b9381b0d5a2060edb6e2ea31351c4b569abf52216d05c91a16214ac3",
|
||||||
"sha256:dce039a8a8a318d7af83cae3fd08d58cefd2120075dfac0ae14d706974040f63",
|
"sha256:e3fe77a6558d21481d4c1290462eb3c9498bad46ce4cd979ff9dd2dd76b5700f",
|
||||||
"sha256:e3213037ea33c85ab705579268cbc8a4433357e9fb99ec7ce9fdcc4d4eec1d50",
|
"sha256:f04359d03506c4a8b0a7d178afda8dca78405bf6f8e8f2080d5206fc7733631a",
|
||||||
"sha256:ec8d8023d31ef72026d46e9fb301ff8759eff5336bcf3d1510836375f53f96a9",
|
"sha256:f066c42bbc84c658d81aaecf36a14b71ef89a0353b17dc5c50f50fd7fb92fc15",
|
||||||
"sha256:ece65730d50aa57a1330d86d81582a2d1587b2ca51cb34f586da8551ddc68fee",
|
"sha256:f26d76c5430efc44b7a3ca4f5f661f3cb314a33bc2a9656f89f44bf73514446c",
|
||||||
"sha256:ed21fc515e224727793e4cc3fb3d00f33f59e3a167d3ad6ac1475ab3b05c2f9e",
|
"sha256:fab723651dfb40d25d8557bc0230133b29cf12835875cd56f77138ee467408cb",
|
||||||
"sha256:eec1132d878153d61a05424f35f089f951bd6095a4f6c60bdd2ef8919d44425e"
|
"sha256:feb7673ee7981cee624c9cdb3446a7d909e99e7296eedef973e766a2bd5128ff"
|
||||||
],
|
],
|
||||||
"version": "==3.7.3"
|
"version": "==3.8.0"
|
||||||
},
|
},
|
||||||
"pyjwkest": {
|
"pyjwkest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -500,10 +507,10 @@
|
|||||||
},
|
},
|
||||||
"sqlalchemy": {
|
"sqlalchemy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:781fb7b9d194ed3fc596b8f0dd4623ff160e3e825dd8c15472376a438c19598b"
|
"sha256:d5432832f91d200c3d8b473a266d59442d825f9ea744c467e68c5d9a9479fbce"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.3.1"
|
"version": "==1.3.2"
|
||||||
},
|
},
|
||||||
"sqlalchemy-migrate": {
|
"sqlalchemy-migrate": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -543,10 +550,10 @@
|
|||||||
},
|
},
|
||||||
"werkzeug": {
|
"werkzeug": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
|
"sha256:0a73e8bb2ff2feecfc5d56e6f458f5b99290ef34f565ffb2665801ff7de6af7a",
|
||||||
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
|
"sha256:7fad9770a8778f9576693f0cc29c7dcc36964df916b83734f4431c0e612a7fbc"
|
||||||
],
|
],
|
||||||
"version": "==0.14.1"
|
"version": "==0.15.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {}
|
"develop": {}
|
||||||
|
|||||||
33
__init__.py
33
__init__.py
@@ -3,8 +3,10 @@
|
|||||||
Backend base module
|
Backend base module
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import Flask
|
import jwt
|
||||||
|
from flask import Flask, jsonify
|
||||||
from flask_httpauth import HTTPTokenAuth, HTTPBasicAuth, MultiAuth
|
from flask_httpauth import HTTPTokenAuth, HTTPBasicAuth, MultiAuth
|
||||||
|
from flask_jwt_extended import JWTManager, decode_token
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
@@ -16,9 +18,32 @@ db = SQLAlchemy(app)
|
|||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
login_manager.init_app(app)
|
login_manager.init_app(app)
|
||||||
|
|
||||||
jwt_auth = HTTPTokenAuth()
|
# flask_jwt_extended: to be used usually by API
|
||||||
|
jwt_extended = JWTManager(app)
|
||||||
|
#
|
||||||
|
jwt_auth = HTTPTokenAuth('Bearer')
|
||||||
|
|
||||||
|
|
||||||
|
@jwt_auth.verify_token
|
||||||
|
def verify_token(token):
|
||||||
|
"""This function (and HTTPTokenAuth('Bearer')) has been defined to be used together with MultiAuth. For API calls
|
||||||
|
solely using JWT authentication, jwt_required of flask_jwt_extended should be used directly."""
|
||||||
|
app.logger.info(token)
|
||||||
|
try:
|
||||||
|
decoded = decode_token(token)
|
||||||
|
except jwt.exceptions.DecodeError as e:
|
||||||
|
app.logger.warn("Could not verify token: {}".format(str(e)))
|
||||||
|
return False
|
||||||
|
except jwt.exceptions.ExpiredSignatureError as e:
|
||||||
|
app.logger.warn("Could not verify token: {}".format(str(e)))
|
||||||
|
return False
|
||||||
|
app.logger.info(decoded)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
basic_auth = HTTPBasicAuth()
|
basic_auth = HTTPBasicAuth()
|
||||||
multi_auth = MultiAuth(basic_auth, jwt_auth)
|
multi_auth = MultiAuth(basic_auth, jwt_auth)
|
||||||
|
|
||||||
from backend.auth import oidc_auth, auth_bp
|
from backend.auth import oidc_auth, auth_bp
|
||||||
|
|
||||||
oidc_auth.init_app(app)
|
oidc_auth.init_app(app)
|
||||||
@@ -26,11 +51,13 @@ oidc_auth.init_app(app)
|
|||||||
# oidc_multi_auth = MultiAuth(oidc_auth, jwt_auth) <- can't work as OIDCAuthentication not implementing HTTPAuth
|
# 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_v1, api_bp
|
||||||
|
|
||||||
app.register_blueprint(auth_bp)
|
app.register_blueprint(auth_bp)
|
||||||
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)
|
||||||
|
|
||||||
|
# Fix flask-restplus by duck typing error handlers
|
||||||
|
jwt_extended._set_error_handler_callbacks(api_v1)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask_restplus import Api
|
from flask_restplus import Api, Namespace
|
||||||
|
|
||||||
api_authorizations = {
|
api_authorizations = {
|
||||||
'apikey': {
|
'apikey': {
|
||||||
@@ -25,7 +25,15 @@ api_bp = Blueprint('api', __name__, url_prefix='/api')
|
|||||||
api_v1 = Api(api_bp, prefix="/v1", version='0.1', title='Vue Test API',
|
api_v1 = Api(api_bp, prefix="/v1", version='0.1', title='Vue Test API',
|
||||||
description='The Vue Test API', doc='/v1/doc/', authorizations=api_authorizations, security='bearerAuth')
|
description='The Vue Test API', doc='/v1/doc/', authorizations=api_authorizations, security='bearerAuth')
|
||||||
|
|
||||||
|
api_user = Namespace('user', description="User management namespace", authorizations=api_authorizations)
|
||||||
|
|
||||||
|
api_v1.add_namespace(api_user)
|
||||||
|
|
||||||
auth_api_bp = Blueprint('auth_api', __name__, url_prefix='/api/auth')
|
auth_api_bp = Blueprint('auth_api', __name__, url_prefix='/api/auth')
|
||||||
|
user_api_bp = Blueprint('user_api', __name__, url_prefix='/api/user')
|
||||||
|
group_api_bp = Blueprint('group_api', __name__, url_prefix='/api/group')
|
||||||
|
|
||||||
from .example_api import *
|
from .example_api import *
|
||||||
from .auth_api import *
|
from .auth_api import *
|
||||||
|
from .user_api import *
|
||||||
|
from .group_api import *
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ For example: listing of available auth providers or registration of users.
|
|||||||
|
|
||||||
Login through API does not start a new session, but instead returns JWT.
|
Login through API does not start a new session, but instead returns JWT.
|
||||||
"""
|
"""
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import jwt
|
import jwt
|
||||||
from flask import request, jsonify, current_app, url_for
|
from flask import request, jsonify, current_app, url_for, Response, session, redirect, make_response
|
||||||
|
from flask_jwt_extended import create_access_token, create_refresh_token, jwt_refresh_token_required, get_jwt_identity
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from random import randint
|
from random import randint
|
||||||
|
|
||||||
@@ -22,14 +26,6 @@ from backend.auth import AUTH_PROVIDERS, oidc_auth
|
|||||||
from backend.models.user_model import User, Group
|
from backend.models.user_model import User, Group
|
||||||
|
|
||||||
|
|
||||||
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',))
|
@auth_api_bp.route('/providers', methods=('GET',))
|
||||||
def get_auth_providers():
|
def get_auth_providers():
|
||||||
providers = dict()
|
providers = dict()
|
||||||
@@ -65,8 +61,11 @@ def login():
|
|||||||
if not user:
|
if not user:
|
||||||
return jsonify({'message': 'Invalid credentials', 'authenticated': False}), 401
|
return jsonify({'message': 'Invalid credentials', 'authenticated': False}), 401
|
||||||
|
|
||||||
token = create_jwt(user)
|
token = {
|
||||||
return jsonify({'token': token.decode('UTF-8')})
|
'access_token': create_access_token(identity=user.email, fresh=True),
|
||||||
|
'refresh_token': create_refresh_token(identity=user.email)
|
||||||
|
}
|
||||||
|
return jsonify(token), 200
|
||||||
|
|
||||||
|
|
||||||
def check_and_create_groups(groups: Iterable[str]):
|
def check_and_create_groups(groups: Iterable[str]):
|
||||||
@@ -112,17 +111,38 @@ def create_or_retrieve_user_from_userinfo(userinfo):
|
|||||||
|
|
||||||
|
|
||||||
@auth_api_bp.route('/oidc', methods=['GET'])
|
@auth_api_bp.route('/oidc', methods=['GET'])
|
||||||
|
@auth_api_bp.route('/oidc/<redirect_url>', methods=['GET'])
|
||||||
@oidc_auth.oidc_auth()
|
@oidc_auth.oidc_auth()
|
||||||
def oidc():
|
def oidc(redirect_url=None):
|
||||||
|
|
||||||
user = create_or_retrieve_user_from_userinfo(flask.session['userinfo'])
|
user = create_or_retrieve_user_from_userinfo(flask.session['userinfo'])
|
||||||
|
|
||||||
#return jsonify(user.to_dict())
|
|
||||||
return user.toJSON()
|
|
||||||
if user is None:
|
if user is None:
|
||||||
return "Could not authenticate: could not find or create user.", 401
|
return "Could not authenticate: could not find or create user.", 401
|
||||||
if current_app.config.get("AUTH_RETURN_EXTERNAL_JWT", False):
|
if current_app.config.get("AUTH_RETURN_EXTERNAL_JWT", False):
|
||||||
token = jwt.encode(flask.session['id_token'], current_app.config['SECRET_KEY'])
|
token = jwt.encode(flask.session['id_token'], current_app.config['SECRET_KEY'])
|
||||||
else:
|
else:
|
||||||
token = create_jwt(user)
|
token = json.dumps({
|
||||||
return token
|
'access_token': create_access_token(identity=user.email, fresh=True),
|
||||||
|
'refresh_token': create_refresh_token(identity=user.email)
|
||||||
|
})
|
||||||
|
if redirect_url is None:
|
||||||
|
redirect_url = request.headers.get("Referer")
|
||||||
|
if redirect_url is None:
|
||||||
|
redirect_url = request.args.get('redirect_url')
|
||||||
|
if redirect_url is None:
|
||||||
|
redirect_url = "/"
|
||||||
|
app.logger.info("Token: {}".format(token))
|
||||||
|
response = make_response(redirect(redirect_url))
|
||||||
|
response.set_cookie('tokens', base64.b64encode(token.encode('utf-8')))
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/refresh', methods=['POST'])
|
||||||
|
@jwt_refresh_token_required
|
||||||
|
def refresh():
|
||||||
|
"""Refresh token endpoint. This will generate a new access token from
|
||||||
|
the refresh token, but will mark that access token as non-fresh,
|
||||||
|
as we do not actually verify a password in this endpoint."""
|
||||||
|
current_user = get_jwt_identity()
|
||||||
|
new_token = create_access_token(identity=current_user, fresh=False)
|
||||||
|
ret = {'access_token': new_token}
|
||||||
|
return jsonify(ret), 200
|
||||||
|
|||||||
40
api/group_api.py
Normal file
40
api/group_api.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
"""
|
||||||
|
This module provides functions related to authentication through the API.
|
||||||
|
For example: listing of available auth providers or registration of users.
|
||||||
|
|
||||||
|
Login through API does not start a new session, but instead returns JWT.
|
||||||
|
"""
|
||||||
|
import flask
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import jwt
|
||||||
|
from flask import request, jsonify, current_app, url_for
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
from functools import wraps
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
from flask_login import logout_user, login_user
|
||||||
|
from typing import Iterable
|
||||||
|
from werkzeug.routing import BuildError
|
||||||
|
|
||||||
|
from backend import db, app
|
||||||
|
from backend.api import auth_api_bp, group_api_bp
|
||||||
|
from backend.auth import AUTH_PROVIDERS, oidc_auth
|
||||||
|
from backend.models.user_model import User, Group
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@group_api_bp.route('/<id>', methods=['GET'])
|
||||||
|
@jwt_required
|
||||||
|
def get_group():
|
||||||
|
|
||||||
|
user = create_or_retrieve_user_from_userinfo(flask.session['userinfo'])
|
||||||
|
|
||||||
|
return jsonify(user.to_dict())
|
||||||
|
if user is None:
|
||||||
|
return "Could not authenticate: could not find or create user.", 401
|
||||||
|
if current_app.config.get("AUTH_RETURN_EXTERNAL_JWT", False):
|
||||||
|
token = jwt.encode(flask.session['id_token'], current_app.config['SECRET_KEY'])
|
||||||
|
else:
|
||||||
|
token = create_jwt(user)
|
||||||
|
return token
|
||||||
58
api/user_api.py
Normal file
58
api/user_api.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
"""
|
||||||
|
This module provides functions related to authentication through the API.
|
||||||
|
For example: listing of available auth providers or registration of users.
|
||||||
|
|
||||||
|
Login through API does not start a new session, but instead returns JWT.
|
||||||
|
"""
|
||||||
|
import flask
|
||||||
|
import jwt
|
||||||
|
from flask import request, jsonify, current_app, url_for
|
||||||
|
from flask_jwt_extended import get_jwt_identity, jwt_optional, jwt_required
|
||||||
|
from flask_restplus import Resource, fields
|
||||||
|
|
||||||
|
from backend import db, app, jwt_auth
|
||||||
|
from backend.api import user_api_bp, api_bp, api_user
|
||||||
|
from backend.auth import oidc_auth
|
||||||
|
from backend.models.user_model import User, Group
|
||||||
|
|
||||||
|
user = api_user.model('User', {
|
||||||
|
'id': fields.String(required=True, description='The user\'s identifier'),
|
||||||
|
'first_name': fields.String(required=True, description='The user\'s first name'),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@api_user.route('/')
|
||||||
|
class UserList(Resource):
|
||||||
|
"""
|
||||||
|
This is a test class.
|
||||||
|
"""
|
||||||
|
#@jwt_auth.login_required
|
||||||
|
@jwt_required
|
||||||
|
@api_user.doc('users')
|
||||||
|
@api_user.marshal_list_with(user)
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
just a test!
|
||||||
|
:return: Hello: World
|
||||||
|
"""
|
||||||
|
current_user = get_jwt_identity()
|
||||||
|
app.logger.info(current_user)
|
||||||
|
return User.get_all()
|
||||||
|
|
||||||
|
|
||||||
|
@api_user.route('/<id>')
|
||||||
|
@api_user.param('id', 'The user identifier')
|
||||||
|
@api_user.response(404, 'User not found')
|
||||||
|
class UserResource(Resource):
|
||||||
|
@jwt_auth.login_required
|
||||||
|
@api_user.doc('get_user')
|
||||||
|
@api_user.marshal_with(user)
|
||||||
|
def get(self, id):
|
||||||
|
"""Fetch a user given its identifier"""
|
||||||
|
user = User.get_by_id(id)
|
||||||
|
if user is not None:
|
||||||
|
return user
|
||||||
|
api_user.abort(404)
|
||||||
|
|
||||||
|
# api_user.add_resource(UserResource, '/')
|
||||||
22
auth/oidc.py
22
auth/oidc.py
@@ -4,7 +4,7 @@ OIDC login auth module
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import jsonify
|
from flask import jsonify, redirect, url_for
|
||||||
from flask_login import login_user
|
from flask_login import login_user
|
||||||
from flask_pyoidc.flask_pyoidc import OIDCAuthentication
|
from flask_pyoidc.flask_pyoidc import OIDCAuthentication
|
||||||
from flask_pyoidc.user_session import UserSession
|
from flask_pyoidc.user_session import UserSession
|
||||||
@@ -15,17 +15,28 @@ from . import auth_bp
|
|||||||
from .oidc_config import PROVIDER_NAME, OIDC_PROVIDERS
|
from .oidc_config import PROVIDER_NAME, OIDC_PROVIDERS
|
||||||
|
|
||||||
|
|
||||||
|
OIDCAuthentication.oidc_auth_orig = OIDCAuthentication.oidc_auth
|
||||||
|
OIDCAuthentication.oidc_logout_orig = OIDCAuthentication.oidc_logout
|
||||||
|
|
||||||
|
|
||||||
def oidc_auth_default_provider(self):
|
def oidc_auth_default_provider(self):
|
||||||
|
"""monkey patch oidc_auth"""
|
||||||
return self.oidc_auth_orig(PROVIDER_NAME)
|
return self.oidc_auth_orig(PROVIDER_NAME)
|
||||||
|
|
||||||
|
|
||||||
OIDCAuthentication.oidc_auth_orig = OIDCAuthentication.oidc_auth
|
def oidc_logout_default_provider(self):
|
||||||
|
"""monkey patch oidc_logout"""
|
||||||
|
return self.oidc_logout_orig(PROVIDER_NAME)
|
||||||
|
|
||||||
|
|
||||||
OIDCAuthentication.oidc_auth = oidc_auth_default_provider
|
OIDCAuthentication.oidc_auth = oidc_auth_default_provider
|
||||||
|
OIDCAuthentication.oidc_logout = oidc_logout_default_provider
|
||||||
|
|
||||||
oidc_auth = OIDCAuthentication(OIDC_PROVIDERS)
|
oidc_auth = OIDCAuthentication(OIDC_PROVIDERS)
|
||||||
|
|
||||||
|
|
||||||
def create_or_retrieve_user_from_userinfo(userinfo):
|
def create_or_retrieve_user_from_userinfo(userinfo):
|
||||||
|
"""Updates and returns ar creates a user from userinfo (part of OIDC token)."""
|
||||||
try:
|
try:
|
||||||
email = userinfo["email"]
|
email = userinfo["email"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -34,6 +45,7 @@ def create_or_retrieve_user_from_userinfo(userinfo):
|
|||||||
|
|
||||||
if user is not None:
|
if user is not None:
|
||||||
app.logger.info("user found")
|
app.logger.info("user found")
|
||||||
|
#TODO: update user!
|
||||||
return user
|
return user
|
||||||
|
|
||||||
user = User(email=email, first_name=userinfo.get("given_name", ""),
|
user = User(email=email, first_name=userinfo.get("given_name", ""),
|
||||||
@@ -57,3 +69,9 @@ def oidc():
|
|||||||
return jsonify(id_token=user_session.id_token,
|
return jsonify(id_token=user_session.id_token,
|
||||||
access_token=flask.session['access_token'],
|
access_token=flask.session['access_token'],
|
||||||
userinfo=user_session.userinfo)
|
userinfo=user_session.userinfo)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/oidc_logout', methods=['GET'])
|
||||||
|
def oidc_logout():
|
||||||
|
oidc_auth.oidc_logout()
|
||||||
|
return redirect('/')
|
||||||
|
|||||||
42
auth/utils.py
Normal file
42
auth/utils.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import flask_jwt_extended
|
||||||
|
from flask_jwt_extended import jwt_optional, get_jwt_identity
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from backend import jwt_auth
|
||||||
|
from backend.models.user_model import User
|
||||||
|
|
||||||
|
|
||||||
|
def requires_permission_level(permission_level):
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if flask_jwt_extended.verify_jwt_in_request():
|
||||||
|
current_user_id = get_jwt_identity()
|
||||||
|
user = User.get_by_identifier(current_user_id)
|
||||||
|
if user is not None:
|
||||||
|
if user.has_permission(permission_level):
|
||||||
|
#for g in user.groups:
|
||||||
|
# if g.permissions
|
||||||
|
#TODO
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
# return FALSE
|
||||||
|
#if not session.get('email'):
|
||||||
|
# return redirect(url_for('users.login'))
|
||||||
|
|
||||||
|
#user = User.find_by_email(session['email'])
|
||||||
|
#elif not user.allowed(access_level):
|
||||||
|
# return redirect(url_for('users.profile', message="You do not have access to that page. Sorry!"))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def require_jwt():
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
return jwt_auth.login_required(jwt_optional(f(*args, **kwargs)))
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
@@ -64,9 +64,11 @@ class Config():
|
|||||||
|
|
||||||
#ASSETS_DEBUG = True
|
#ASSETS_DEBUG = True
|
||||||
|
|
||||||
JWT_SECRET = "abcxyz"
|
#JWT_SECRET = "abcxyz"
|
||||||
JWT_ALGORITHM = "HS256"
|
#JWT_ALGORITHM = "HS256"
|
||||||
JWT_EXP_DELTA_SECONDS = 5 * 60
|
#JWT_EXP_DELTA_SECONDS = 5 * 60
|
||||||
|
|
||||||
|
JWT_SECRET_KEY = "abcxyz"
|
||||||
|
|
||||||
AUTH_RETURN_EXTERNAL_JWT = False
|
AUTH_RETURN_EXTERNAL_JWT = False
|
||||||
|
|
||||||
|
|||||||
@@ -46,23 +46,38 @@ user_group_table = db.Table('user_group',
|
|||||||
primary_key=True))
|
primary_key=True))
|
||||||
|
|
||||||
|
|
||||||
|
# This is the association table for the many-to-many relationship between
|
||||||
|
# groups and permissions.
|
||||||
|
group_permission_table = db.Table('group_permission',
|
||||||
|
db.Column('group_id', db.Integer,
|
||||||
|
db.ForeignKey('group.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True),
|
||||||
|
db.Column('permission_id', db.Integer,
|
||||||
|
db.ForeignKey('permission.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True))
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin, db.Model):
|
class User(UserMixin, db.Model):
|
||||||
"""
|
"""
|
||||||
Example user model representation.
|
Example user model representation.
|
||||||
"""
|
"""
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
social_id = db.Column(db.String(64), nullable=True, unique=True)
|
social_id = db.Column(db.Unicode(63), nullable=True, unique=True)
|
||||||
nickname = db.Column(db.String(64), index=True, unique=True)
|
nickname = db.Column(db.Unicode(63), index=True, unique=True)
|
||||||
first_name = db.Column(db.String(64), index=True, nullable=True)
|
first_name = db.Column(db.Unicode(63), index=True, nullable=True)
|
||||||
last_name = db.Column(db.String(64), index=True, nullable=True)
|
last_name = db.Column(db.Unicode(63), index=True, nullable=True)
|
||||||
email = db.Column(db.String(120), nullable=False, index=True, unique=True)
|
email = db.Column(db.String(120), nullable=False, index=True, unique=True)
|
||||||
lang = db.Column(db.String(16), index=False, unique=False)
|
lang = db.Column(db.Unicode(32), index=False, unique=False)
|
||||||
timezone = db.Column(db.String(32), index=False, unique=False)
|
timezone = db.Column(db.Unicode(63), index=False, unique=False)
|
||||||
posts = db.relationship('Post', backref='author', lazy='dynamic')
|
posts = db.relationship('Post', backref='author', lazy='dynamic')
|
||||||
example_data_item = db.relationship('ExampleDataItem', backref='owner')
|
example_data_item = db.relationship('ExampleDataItem', backref='owner')
|
||||||
example_data_item_id = db.Column(db.ForeignKey(ExampleDataItem.id))
|
example_data_item_id = db.Column(db.ForeignKey(ExampleDataItem.id))
|
||||||
about_me = db.Column(db.String(140))
|
about_me = db.Column(db.Unicode(255))
|
||||||
role = db.Column(db.String(64))
|
role = db.Column(db.Unicode(63))
|
||||||
groups = db.relationship('Group', secondary=user_group_table, back_populates='users')
|
groups = db.relationship('Group', secondary=user_group_table, back_populates='users')
|
||||||
password = db.Column(db.String(255), nullable=True)
|
password = db.Column(db.String(255), nullable=True)
|
||||||
registered_on = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
|
registered_on = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
|
||||||
@@ -110,6 +125,16 @@ class User(UserMixin, db.Model):
|
|||||||
User.email == identifier,
|
User.email == identifier,
|
||||||
User.id == identifier)).first()
|
User.id == identifier)).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@login_manager.user_loader
|
||||||
|
def get_by_id(identifier):
|
||||||
|
"""
|
||||||
|
Find user by ID.
|
||||||
|
:param identifier:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return User.query.filter(User.id == identifier).first()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all():
|
def get_all():
|
||||||
"""
|
"""
|
||||||
@@ -396,8 +421,9 @@ class Group(db.Model):
|
|||||||
super(Group, self).__init__(**kwargs)
|
super(Group, self).__init__(**kwargs)
|
||||||
|
|
||||||
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
|
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
|
||||||
name = db.Column(db.Unicode(64), unique=True, nullable=False)
|
name = db.Column(db.Unicode(63), unique=True, nullable=False)
|
||||||
users = db.relationship('User', secondary=user_group_table, back_populates='groups')
|
users = db.relationship('User', secondary=user_group_table, back_populates='groups')
|
||||||
|
permissions = db.relationship('Permission', secondary=group_permission_table, back_populates='groups')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_by_name(name):
|
def get_by_name(name):
|
||||||
@@ -417,3 +443,12 @@ class Group(db.Model):
|
|||||||
def toJSON(self):
|
def toJSON(self):
|
||||||
return json.dumps(self.to_dict(), default=lambda o: o.__dict__,
|
return json.dumps(self.to_dict(), default=lambda o: o.__dict__,
|
||||||
sort_keys=True, indent=4)
|
sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
class Permission(db.Model):
|
||||||
|
"""Table containing permissions associated with groups."""
|
||||||
|
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
|
||||||
|
name = db.Column(db.Unicode(63), unique=True, nullable=False)
|
||||||
|
description = db.Column(db.Unicode(255))
|
||||||
|
groups = db.relationship(Group, secondary=group_permission_table,
|
||||||
|
back_populates='permissions')
|
||||||
|
|||||||
Reference in New Issue
Block a user