From 48bf4f1e210d5e5e1a407d31356983c3ce0825c5 Mon Sep 17 00:00:00 2001 From: Mart van Santen <mart@greenhost.nl> Date: Thu, 2 Dec 2021 02:17:00 +0100 Subject: [PATCH] Make is possible to set a password --- login/app.py | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 1 deletion(-) diff --git a/login/app.py b/login/app.py index 68f4528..87d0758 100644 --- a/login/app.py +++ b/login/app.py @@ -6,6 +6,10 @@ import logging import os import click import urllib.parse +import urllib.request +import json +import re +from urllib.request import Request # Flask from flask import abort, Flask, redirect, request, render_template @@ -30,6 +34,8 @@ from ory_kratos_client.model.inline_response503 import InlineResponse503 from ory_kratos_client.model.admin_create_identity_body import AdminCreateIdentityBody from ory_kratos_client.model.admin_create_self_service_recovery_link_body import AdminCreateSelfServiceRecoveryLinkBody +from ory_kratos_client.model.submit_self_service_recovery_flow_body import SubmitSelfServiceRecoveryFlowBody +from ory_kratos_client.model.self_service_recovery_flow import SelfServiceRecoveryFlow from ory_kratos_client.model.identity_state import IdentityState @@ -150,7 +156,162 @@ app.cli.add_command(app_cli) @user_cli.command('create') @click.argument('email') -def create_user(email): +@click.argument('password') +def create_user(email, password): + app.logger.info("Creating user with email: ({0})".format(email)) + + + # Trying to create idenity + try: + body = AdminCreateIdentityBody( + schema_id="default", + traits={'email':email}, + ) # AdminCreateIdentityBody | (optional) + kratos_obj = KRATOS_ADMIN.admin_create_identity(admin_create_identity_body=body) + except ory_kratos_client.exceptions.ApiException as err: + if err.status == 409: + print("Conflict during creation of user. User already exists?") + + + # Kratos does not provide an interface to set a password directly. However + # we still want to be able to set a password. So we have to hack our way + # a bit arround this. We do this by creating a recovery link though the + # admin interface (which is not e-mailed) and then follow the recovery + # flow in the public facing pages of kratos + + + # Step 1: Get all recovery link for this user account + url = "" + try: + # Get out user ID by iterating over all available IDs + data = KRATOS_ADMIN.admin_list_identities() + for id in data.value: + # Unique identifier we use + if (id.traits['email'] == email): + kratos_id = id.id + + # Create body request to get recovery link with admin API + body = AdminCreateSelfServiceRecoveryLinkBody( + expires_in="15m", + identity_id=str(kratos_id) + ) + + # Get recovery link from admin API + api = KRATOS_ADMIN.admin_create_self_service_recovery_link( + admin_create_self_service_recovery_link_body=body) + + url = api.recovery_link + except: + print("Unable to find user and/or create recovery link") + return False + + + # Step 2: Open the recovery link and extract the cookies, as we need them + # for the next steps + try: + # We override the default Redirect handler with our custom handler to + # be able to catch the cookies. + opener = urllib.request.build_opener(RedirectFilter) + req = opener.open(url) + # If we do not have a 2xx status, urllib throws an error, as we "stopped" + # at our redirect, we expect a 3xx status + except urllib.error.HTTPError as http: + cookies = http.headers.get_all('Set-Cookie') + url = http.headers.get('Location') + else: + print("Unable to follow recovery link and get session cookies") + return False + + + # Step 3: Extract cookies and data for next step. We expect to have an + # authorized session now. We need the cookies for followup calls + # to make changes to the account (set password) + + # Get flow id + m = re.match(r'.*\?flow=(.*)', url) + if m: + flow = m.group(1) + else: + print("Unable to extract flow ID") + return False + + # Find kratos session cookie & csrf + for cookie in cookies: + m = re.match(r'ory_kratos_session=([^;]*);.*$', cookie) + if m: + session = "ory_kratos_session=" + m.group(1) + m = re.match(r'(csrf_token[^;]*);.*$', cookie) + if m: + csrf = m.group(1) + + # Combined the relevant cookies + cookie = csrf + "; " + session; + + + # Step 4: Get the "UI", kratos expect us to call the API to get the UI + # elements which inclused the CSRF token, which is needed when + # posting the password data + try: + url = app.config["KRATOS_PUBLIC_URL"] + url = url + "/self-service/settings/flows?id=" + flow + + req = Request(url, headers={'Cookie':cookie}) + opener = urllib.request.build_opener() + + # Execute the request, read the data, decode the JSON, get the + # right CSRF token out of the decoded JSON + http = opener.open(req) + data = http.read() + obj = json.loads(data) + token = obj['ui']['nodes'][0]['attributes']['value'] + + except: + print("Unable to request password reset and get CSRF token") + return False + + + # Step 5: Post out password + + url = app.config["KRATOS_PUBLIC_URL"] + url = url + "self-service/settings?flow=" + flow + + # Create POST data as form data + data = { + 'method': 'password', + 'password': password, + 'csrf_token': token + } + data = urllib.parse.urlencode(data) + data = data.encode('ascii') + + # POST the new password + try: + req = Request(url, data = data, headers={'Cookie':cookie}, method="POST") + opener = urllib.request.build_opener(RedirectFilter) + http = opener.open(req) + # If we do not have a 2xx status, urllib throws an error, as we "stopped" + # at our redirect, we expect a 3xx status + except urllib.error.HTTPError as http: + if http.status == 302: + print("Password is set!") + elif http.status == 303: + print("Password not set. To simple?") + else: + print("Password not set, unknown error") + + else: + print("Unable to follow recovery link and get session cookies") + return False + + + + + + + +@user_cli.command('invite') +@click.argument('email') +def invite_user(email): app.logger.info("Creating user with email: ({0})".format(email)) obj = User() @@ -539,3 +700,10 @@ if __name__ == '__main__': app.run() +# Instead of processing the redirect, we return, so the application +# can handle the redirect itself. This is needed to extract cookies +# etc. +class RedirectFilter(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, hdrs, newurl): + return None + -- GitLab