From 2160f634d1a4a33a8acfbcdd33bb75c80ff06942 Mon Sep 17 00:00:00 2001 From: Luka Date: Tue, 18 Jan 2022 09:48:18 +0000 Subject: [PATCH] Implemented oidc with hydra --- app.py | 3 +++ areas/auth/auth.py | 27 +++++++++++++++----------- config.py | 8 ++++++-- helpers/__init__.py | 1 + helpers/error_handler.py | 14 ++++++++++++-- helpers/hydra_oauth.py | 41 ++++++++++++++++++++++++++++++++++++++++ helpers/kratos_api.py | 2 ++ requirements.txt | 2 ++ run_app.sh | 5 ++++- 9 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 helpers/hydra_oauth.py diff --git a/app.py b/app.py index b69248b..611bc5a 100644 --- a/app.py +++ b/app.py @@ -13,10 +13,12 @@ from areas import auth from helpers import ( BadRequest, KratosError, + HydraError, bad_request_error, validation_error, kratos_error, global_error, + hydra_error, ) from config import * @@ -30,6 +32,7 @@ app.register_error_handler(Exception, global_error) app.register_error_handler(BadRequest, bad_request_error) app.register_error_handler(ValidationError, validation_error) app.register_error_handler(KratosError, kratos_error) +app.register_error_handler(HydraError, hydra_error) jwt = JWTManager(app) diff --git a/areas/auth/auth.py b/areas/auth/auth.py index af89132..2bfd938 100644 --- a/areas/auth/auth.py +++ b/areas/auth/auth.py @@ -1,21 +1,26 @@ -from flask import request, jsonify +from flask import jsonify from flask_jwt_extended import create_access_token from flask_cors import cross_origin +from datetime import timedelta from areas import api_v1 - -USERNAME = 'admin' -PASSWORD = 'admin' +from config import * +from helpers import HydraOauth -@api_v1.route('/login', methods=['POST']) +@api_v1.route("/login", methods=["POST"]) @cross_origin() def login(): - username = request.json.get('username') - password = request.json.get('password') + authorization_url = HydraOauth.authorize() + return jsonify({"authorizationUrl": authorization_url}) - if username != USERNAME or password != PASSWORD: - return jsonify({'errorMessage': 'Invalid username or password'}), 401 - access_token = create_access_token(identity=username) - return jsonify({'username': USERNAME, 'access_token': access_token}) +@api_v1.route("/hydra/callback") +@cross_origin() +def hydra_callback(): + token = HydraOauth.get_token() + access_token = create_access_token( + identity=token, expires_delta=timedelta(days=365) + ) + + return jsonify({"access_token": access_token}) diff --git a/config.py b/config.py index c902400..22a643f 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,8 @@ import os -SECRET_KEY = os.environ.get('SECRET_KEY') -KRATOS_URL = os.environ.get('KRATOS_URL') +SECRET_KEY = os.environ.get("SECRET_KEY") +KRATOS_URL = os.environ.get("KRATOS_URL") +HYDRA_CLIENT_ID = os.environ.get("HYDRA_CLIENT_ID") +HYDRA_CLIENT_SECRET = os.environ.get("HYDRA_CLIENT_SECRET") +HYDRA_AUTHORIZATION_BASE_URL = os.environ.get("HYDRA_AUTHORIZATION_BASE_URL") +TOKEN_URL = os.environ.get("TOKEN_URL") diff --git a/helpers/__init__.py b/helpers/__init__.py index 6743363..8501013 100644 --- a/helpers/__init__.py +++ b/helpers/__init__.py @@ -1,2 +1,3 @@ from .kratos_api import * from .error_handler import * +from .hydra_oauth import * diff --git a/helpers/error_handler.py b/helpers/error_handler.py index 69c6c4d..e6c696f 100644 --- a/helpers/error_handler.py +++ b/helpers/error_handler.py @@ -6,6 +6,10 @@ class KratosError(Exception): pass +class HydraError(Exception): + pass + + class BadRequest(Exception): pass @@ -24,11 +28,17 @@ def validation_error(e): def kratos_error(e): - message = e.args[0] if e.args else "Failed to contact Kratos." + message = "[KratosError] " + e.args[0] if e.args else "Failed to contact Kratos." + status_code = e.args[1] if e.args else 500 + return jsonify({"errorMessage": message}), status_code + + +def hydra_error(e): + message = "[HydraError] " + e.args[0] if e.args else "Failed to contact Hydra." status_code = e.args[1] if e.args else 500 return jsonify({"errorMessage": message}), status_code def global_error(e): message = str(e) - return jsonify({"errorMessage": message}) + return jsonify({"errorMessage": message}), 500 diff --git a/helpers/hydra_oauth.py b/helpers/hydra_oauth.py new file mode 100644 index 0000000..ea84695 --- /dev/null +++ b/helpers/hydra_oauth.py @@ -0,0 +1,41 @@ +from flask import request, session +from requests_oauthlib import OAuth2Session + +from config import * +from helpers import HydraError + + +class HydraOauth: + SESSION_KEY = "oauth_state" + + @staticmethod + def authorize(): + try: + hydra = OAuth2Session(HYDRA_CLIENT_ID) + authorization_url, state = hydra.authorization_url( + HYDRA_AUTHORIZATION_BASE_URL + ) + + # State is used to prevent CSRF, keep this for later. + session[HydraOauth.SESSION_KEY] = state + + return authorization_url + except Exception as err: + raise HydraError(str(err), 500) + + @staticmethod + def get_token(): + try: + hydra = OAuth2Session( + HYDRA_CLIENT_ID, state=session[HydraOauth.SESSION_KEY] + ) + token = hydra.fetch_token( + TOKEN_URL, + client_secret=HYDRA_CLIENT_SECRET, + authorization_response=request.url, + ) + + session["hydra_token"] = token + return token + except Exception as err: + raise HydraError(str(err), 500) diff --git a/helpers/kratos_api.py b/helpers/kratos_api.py index 739f9a0..87b1e9d 100644 --- a/helpers/kratos_api.py +++ b/helpers/kratos_api.py @@ -18,6 +18,8 @@ class KratosApi: res = requests.get("{}{}".format(KRATOS_URL, url)) KratosApi.__handleError(res) return res + except KratosError as err: + raise err except: raise KratosError() diff --git a/requirements.txt b/requirements.txt index 8510b35..b346d6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ jsonschema==4.3.2 Jinja2==3.0.3 MarkupSafe==2.0.1 mypy-extensions==0.4.3 +oauthlib==3.1.1 pathspec==0.9.0 platformdirs==2.4.0 pycparser==2.21 @@ -24,6 +25,7 @@ PyJWT==2.3.0 pyrsistent==0.18.0 regex==2021.11.10 requests==2.26.0 +requests-oauthlib==1.3.0 six==1.16.0 tomli==1.2.3 typing-extensions==4.0.1 diff --git a/run_app.sh b/run_app.sh index b8f5f49..302f141 100755 --- a/run_app.sh +++ b/run_app.sh @@ -22,5 +22,8 @@ export FLASK_APP=app.py export FLASK_ENV=development export SECRET_KEY="e38hq!@0n64g@qe6)5csk41t=ljo2vllog(%k7njnm4b@kh42c" export KRATOS_URL="http://127.0.0.1:8000" - +export HYDRA_CLIENT_ID="dashboard" +export HYDRA_CLIENT_SECRET="BrYRtKygtrcwGHviUSqybvFTgfnaZgPh" +export HYDRA_AUTHORIZATION_BASE_URL="https://sso.init.stackspin.net/oauth2/auth" +export TOKEN_URL="https://sso.init.stackspin.net/oauth2/token" flask run