From bf98fbd7216d9439fc3aae0439f20b181c60ff4a Mon Sep 17 00:00:00 2001 From: Maarten de Waard Date: Fri, 29 Apr 2022 15:29:18 +0200 Subject: [PATCH] feat: add error handling for unaccepted passwords, add kratos error page --- DEVELOPMENT.md | 2 +- areas/users/user_service.py | 8 ++--- run_app.sh | 2 +- web/login/login.py | 19 +++++++++++ web/static/base.js | 66 ++++++++++++++++++++++++++----------- web/templates/error.html | 23 +++++++++++++ 6 files changed, 95 insertions(+), 25 deletions(-) create mode 100644 web/templates/error.html diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index e5e605d..f5b4aa6 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -236,7 +236,7 @@ cat source_env.local export HYDRA_ADMIN_URL=http://localhost:4445 export KRATOS_PUBLIC_URL=http://localhost/api -export KRATOS_ADMIN_URL=http://localhost:8000/admin +export KRATOS_ADMIN_URL=http://localhost:8000 export LOGIN_PANEL_URL=http://localhost/web export DATABASE_URL="mysql+pymysql://stackspin:stackspin@localhost/stackspin" ``` diff --git a/areas/users/user_service.py b/areas/users/user_service.py index 8e1dd5a..74f0bc4 100644 --- a/areas/users/user_service.py +++ b/areas/users/user_service.py @@ -8,7 +8,7 @@ from helpers import KratosApi class UserService: @staticmethod def get_users(): - res = KratosApi.get("/identities").json() + res = KratosApi.get("/admin/identities").json() userList = [] for r in res: userList.append(UserService.__insertAppRoleToUser(r["id"], r)) @@ -17,7 +17,7 @@ class UserService: @staticmethod def get_user(id): - res = KratosApi.get("/identities/{}".format(id)).json() + res = KratosApi.get("/admin/identities/{}".format(id)).json() return UserService.__insertAppRoleToUser(id, res) @staticmethod @@ -26,7 +26,7 @@ class UserService: "schema_id": "default", "traits": {"email": data["email"], "name": data["name"]}, } - res = KratosApi.post("/identities", kratos_data).json() + res = KratosApi.post("/admin/identities", kratos_data).json() appRole = AppRole( user_id=res["id"], @@ -45,7 +45,7 @@ class UserService: "schema_id": "default", "traits": {"email": data["email"], "name": data["name"]}, } - KratosApi.put("/identities/{}".format(id), kratos_data) + KratosApi.put("/admin/identities/{}".format(id), kratos_data) app_role = AppRole.query.filter_by(user_id=id).first() if app_role: diff --git a/run_app.sh b/run_app.sh index 7710af3..4d6074a 100755 --- a/run_app.sh +++ b/run_app.sh @@ -28,7 +28,7 @@ export TOKEN_URL="https://sso.init.stackspin.net/oauth2/token" # Login facilitator paths export KRATOS_PUBLIC_URL=http://localhost/kratos -export KRATOS_ADMIN_URL=http://localhost:8000/admin +export KRATOS_ADMIN_URL=http://localhost:8000 export HYDRA_PUBLIC_URL="https://sso.init.stackspin.net" export HYDRA_ADMIN_URL=http://localhost:4445 export LOGIN_PANEL_URL=http://localhost/web/ diff --git a/web/login/login.py b/web/login/login.py index 8f8f7e0..7874f8e 100644 --- a/web/login/login.py +++ b/web/login/login.py @@ -71,6 +71,25 @@ def settings(): return render_template("settings.html", api_url=KRATOS_PUBLIC_URL) +@web.route("/error", methods=["GET"]) +def error(): + """Show error messages from Kratos + + Implements user-facing errors as described in https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors + + :param id: error ID as given by Kratos + :return: redirect or settings page + """ + + error_id = request.args.get("id") + api_response="" + try: + # Get Self-Service Errors + api_response = KRATOS_ADMIN.get_self_service_error(error_id) + except ory_kratos_client.ApiException as ex: + print("Exception when calling V0alpha2Api->get_self_service_error: %s\n" % ex) + + return render_template("error.html", error_message=api_response) @web.route("/login", methods=["GET", "POST"]) def login(): diff --git a/web/static/base.js b/web/static/base.js index a98e4a0..6d94cea 100644 --- a/web/static/base.js +++ b/web/static/base.js @@ -56,8 +56,8 @@ function flow_login() { url: uri, success: function(data) { - // Render login form (group: profile) - var html = render_form(data, 'profile'); + // Render login form (group: password) + var html = render_form(data, 'password'); $("#contentLogin").html(html); }, @@ -94,11 +94,15 @@ function flow_settings_validate() { window.location.href = 'settings'; } else { + // There was an error, Kratos does not specify what is + // wrong. So we just show the general error message and + // let the user figure it out. We can re-use the flow-id + $("#contentProfileSaveFailed").show(); - // There was an error, Kratos does not specify what is - // wrong. So we just show the general error message and - // let the user figure it out. We can re-use the flow-id - $("#contentProfileSaveFailed").show(); + // For now, this code assumes that only the password can fail + // validation. Other forms might need to be added in the future. + html = render_form(data, 'password') + $("#contentPassword").html(html) } } }); @@ -134,12 +138,10 @@ function flow_settings() { } - // FIXME: This seems to be not necessary anymore in kratos 0.9.0 - // because they moved the password field to the profile group // Render the password & profile form based on the fields we got // from the API - // var html = render_form(data, 'profile'); - // $("#contentPassword").html(html); + var html = render_form(data, 'password'); + $("#contentPassword").html(html); html = render_form(data, 'profile'); $("#contentProfile").html(html); @@ -251,9 +253,10 @@ function render_form(data, group) { var name = node.attributes.name; var type = node.attributes.type; var value = node.attributes.value; + var messages = node.messages if (node.group == 'default' || node.group == group) { - var elm = getFormElement(type, name, value); + var elm = getFormElement(type, name, value, messages); form += elm; } } @@ -271,11 +274,18 @@ function render_form(data, group) { // like "email" are also supported // name: name of the field. Used when posting data // value: If there is already a value known, show it -function getFormElement(type, name, value) { +// messages: error messages related to the field +function getFormElement(type, name, value, messages) { + console.log("Getting form element", type, name, value, messages) if (value == undefined) { value = ''; } + + if (typeof(messages) == "undefined") { + messages = [] + } + if (name == 'email' || name == 'traits.email') { return getFormInput( 'email', @@ -285,6 +295,7 @@ function getFormElement(type, name, value) { 'Please enter your e-mail address here', 'Please provide your e-mail address. We will send a recovery ' + 'link to that e-mail address.', + messages, ); } @@ -295,7 +306,8 @@ function getFormElement(type, name, value) { value, 'Username', 'Please provide an username', - null + null, + messages, ); } @@ -306,7 +318,8 @@ function getFormElement(type, name, value) { value, 'Full name', 'Please provide your full name', - null + null, + messages, ); } @@ -317,8 +330,9 @@ function getFormElement(type, name, value) { name, value, 'E-mail address', - 'Please provide your e-mail address to login', - null + 'Please provide your e-mail address to log in', + null, + messages, ); } @@ -329,7 +343,8 @@ function getFormElement(type, name, value) { value, 'Password', 'Please provide your password', - null + null, + messages, ); } @@ -350,7 +365,7 @@ function getFormElement(type, name, value) { } - return getFormInput('input', name, value, name, null,null); + return getFormInput('input', name, value, name, null,null, messages); } @@ -363,7 +378,12 @@ function getFormElement(type, name, value) { // param label: Label to display above field // param placeHolder: Label to display in field if empty // param help: Additional help text, displayed below the field in small font -function getFormInput(type, name, value, label, placeHolder, help) { +// param messages: Message about failed input +function getFormInput(type, name, value, label, placeHolder, help, messages) { + if (typeof(help) == "undefined" || help == null) { + help = "" + } + console.log("Messages: ", messages); // Id field for help element var nameHelp = name + "Help"; @@ -372,6 +392,14 @@ function getFormInput(type, name, value, label, placeHolder, help) { element += ''; element += ' + var api_url = '{{ api_url }}'; + + // Actions + $(document).ready(function() { + flow_settings(); + }); + + + +

Error: {{ error_message['error']['status'] }}

+ + +
+ {{ error_message['error']['message'] }} + {{ error_message['error']['reason'] }} +
+{% endblock %} +