feat: add error handling for unaccepted passwords, add kratos error page

This commit is contained in:
Maarten de Waard 2022-04-29 15:29:18 +02:00
parent c120ab603a
commit bf98fbd721
No known key found for this signature in database
GPG key ID: 1D3E893A657CC8DA
6 changed files with 95 additions and 25 deletions

View file

@ -236,7 +236,7 @@ cat source_env.local
export HYDRA_ADMIN_URL=http://localhost:4445 export HYDRA_ADMIN_URL=http://localhost:4445
export KRATOS_PUBLIC_URL=http://localhost/api 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 LOGIN_PANEL_URL=http://localhost/web
export DATABASE_URL="mysql+pymysql://stackspin:stackspin@localhost/stackspin" export DATABASE_URL="mysql+pymysql://stackspin:stackspin@localhost/stackspin"
``` ```

View file

@ -8,7 +8,7 @@ from helpers import KratosApi
class UserService: class UserService:
@staticmethod @staticmethod
def get_users(): def get_users():
res = KratosApi.get("/identities").json() res = KratosApi.get("/admin/identities").json()
userList = [] userList = []
for r in res: for r in res:
userList.append(UserService.__insertAppRoleToUser(r["id"], r)) userList.append(UserService.__insertAppRoleToUser(r["id"], r))
@ -17,7 +17,7 @@ class UserService:
@staticmethod @staticmethod
def get_user(id): def get_user(id):
res = KratosApi.get("/identities/{}".format(id)).json() res = KratosApi.get("/admin/identities/{}".format(id)).json()
return UserService.__insertAppRoleToUser(id, res) return UserService.__insertAppRoleToUser(id, res)
@staticmethod @staticmethod
@ -26,7 +26,7 @@ class UserService:
"schema_id": "default", "schema_id": "default",
"traits": {"email": data["email"], "name": data["name"]}, "traits": {"email": data["email"], "name": data["name"]},
} }
res = KratosApi.post("/identities", kratos_data).json() res = KratosApi.post("/admin/identities", kratos_data).json()
appRole = AppRole( appRole = AppRole(
user_id=res["id"], user_id=res["id"],
@ -45,7 +45,7 @@ class UserService:
"schema_id": "default", "schema_id": "default",
"traits": {"email": data["email"], "name": data["name"]}, "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() app_role = AppRole.query.filter_by(user_id=id).first()
if app_role: if app_role:

View file

@ -28,7 +28,7 @@ export TOKEN_URL="https://sso.init.stackspin.net/oauth2/token"
# Login facilitator paths # Login facilitator paths
export KRATOS_PUBLIC_URL=http://localhost/kratos 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_PUBLIC_URL="https://sso.init.stackspin.net"
export HYDRA_ADMIN_URL=http://localhost:4445 export HYDRA_ADMIN_URL=http://localhost:4445
export LOGIN_PANEL_URL=http://localhost/web/ export LOGIN_PANEL_URL=http://localhost/web/

View file

@ -71,6 +71,25 @@ def settings():
return render_template("settings.html", api_url=KRATOS_PUBLIC_URL) 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"]) @web.route("/login", methods=["GET", "POST"])
def login(): def login():

View file

@ -56,8 +56,8 @@ function flow_login() {
url: uri, url: uri,
success: function(data) { success: function(data) {
// Render login form (group: profile) // Render login form (group: password)
var html = render_form(data, 'profile'); var html = render_form(data, 'password');
$("#contentLogin").html(html); $("#contentLogin").html(html);
}, },
@ -94,11 +94,15 @@ function flow_settings_validate() {
window.location.href = 'settings'; window.location.href = 'settings';
} }
else { 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 // For now, this code assumes that only the password can fail
// wrong. So we just show the general error message and // validation. Other forms might need to be added in the future.
// let the user figure it out. We can re-use the flow-id html = render_form(data, 'password')
$("#contentProfileSaveFailed").show(); $("#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 // Render the password & profile form based on the fields we got
// from the API // from the API
// var html = render_form(data, 'profile'); var html = render_form(data, 'password');
// $("#contentPassword").html(html); $("#contentPassword").html(html);
html = render_form(data, 'profile'); html = render_form(data, 'profile');
$("#contentProfile").html(html); $("#contentProfile").html(html);
@ -251,9 +253,10 @@ function render_form(data, group) {
var name = node.attributes.name; var name = node.attributes.name;
var type = node.attributes.type; var type = node.attributes.type;
var value = node.attributes.value; var value = node.attributes.value;
var messages = node.messages
if (node.group == 'default' || node.group == group) { if (node.group == 'default' || node.group == group) {
var elm = getFormElement(type, name, value); var elm = getFormElement(type, name, value, messages);
form += elm; form += elm;
} }
} }
@ -271,11 +274,18 @@ function render_form(data, group) {
// like "email" are also supported // like "email" are also supported
// name: name of the field. Used when posting data // name: name of the field. Used when posting data
// value: If there is already a value known, show it // 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) { if (value == undefined) {
value = ''; value = '';
} }
if (typeof(messages) == "undefined") {
messages = []
}
if (name == 'email' || name == 'traits.email') { if (name == 'email' || name == 'traits.email') {
return getFormInput( return getFormInput(
'email', 'email',
@ -285,6 +295,7 @@ function getFormElement(type, name, value) {
'Please enter your e-mail address here', 'Please enter your e-mail address here',
'Please provide your e-mail address. We will send a recovery ' + 'Please provide your e-mail address. We will send a recovery ' +
'link to that e-mail address.', 'link to that e-mail address.',
messages,
); );
} }
@ -295,7 +306,8 @@ function getFormElement(type, name, value) {
value, value,
'Username', 'Username',
'Please provide an username', 'Please provide an username',
null null,
messages,
); );
} }
@ -306,7 +318,8 @@ function getFormElement(type, name, value) {
value, value,
'Full name', 'Full name',
'Please provide your full name', 'Please provide your full name',
null null,
messages,
); );
} }
@ -317,8 +330,9 @@ function getFormElement(type, name, value) {
name, name,
value, value,
'E-mail address', 'E-mail address',
'Please provide your e-mail address to login', 'Please provide your e-mail address to log in',
null null,
messages,
); );
} }
@ -329,7 +343,8 @@ function getFormElement(type, name, value) {
value, value,
'Password', 'Password',
'Please provide your 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 label: Label to display above field
// param placeHolder: Label to display in field if empty // param placeHolder: Label to display in field if empty
// param help: Additional help text, displayed below the field in small font // 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 // Id field for help element
var nameHelp = name + "Help"; var nameHelp = name + "Help";
@ -372,6 +392,14 @@ function getFormInput(type, name, value, label, placeHolder, help) {
element += '<label for="'+name+'">'+label+'</label>'; element += '<label for="'+name+'">'+label+'</label>';
element += '<input type="'+type+'" class="form-control" id="'+name+'" name="'+name+'" '; element += '<input type="'+type+'" class="form-control" id="'+name+'" name="'+name+'" ';
// messages get appended to help info
if (messages.length) {
for (message in messages) {
console.log("adding message", messages[message])
help += messages[message]['text']
}
}
// If we are a password field, add a eye icon to reveal password // If we are a password field, add a eye icon to reveal password
if (value) { if (value) {
element += 'value="'+value+'" '; element += 'value="'+value+'" ';

23
web/templates/error.html Normal file
View file

@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% block content %}
<script>
var api_url = '{{ api_url }}';
// Actions
$(document).ready(function() {
flow_settings();
});
</script>
<h2>Error: {{ error_message['error']['status'] }}</h2>
<div class=error-div>
{{ error_message['error']['message'] }}
{{ error_message['error']['reason'] }}
</div>
{% endblock %}