From 2160f634d1a4a33a8acfbcdd33bb75c80ff06942 Mon Sep 17 00:00:00 2001
From: Luka <luka@init.hr>
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 b69248b6..611bc5a3 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 af89132c..2bfd938d 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
+from config import *
+from helpers import HydraOauth
 
-USERNAME = 'admin'
-PASSWORD = 'admin'
 
-
-@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
+@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)
+    )
 
-    access_token = create_access_token(identity=username)
-    return jsonify({'username': USERNAME, 'access_token': access_token})
+    return jsonify({"access_token": access_token})
diff --git a/config.py b/config.py
index c902400e..22a643fd 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 67433630..85010134 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 69c6c4dc..e6c696f4 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 00000000..ea846951
--- /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 739f9a03..87b1e9d4 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 8510b35c..b346d6de 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 b8f5f497..302f141d 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
-- 
GitLab