diff --git a/config.py b/config.py
index 24bd560626a6376d63316a50a1e32cc34f34cdfd..d830ac717501e411dccb52d7df8daa6cf919ab16 100644
--- a/config.py
+++ b/config.py
@@ -22,9 +22,9 @@ MGMT_USER_KEY_PATH = os.path.join(SOCTOOLS_BASE, "secrets/CA/private/soctools-us
 # MGMT_USER_CERT_PATH = os.path.join(SOCTOOLS_BASE, "secrets/certificates/SOC_Admin.crt.pem")
 # MGMT_USER_KEY_PATH = os.path.join(SOCTOOLS_BASE, "secrets/certificates/SOC_Admin.key.pem")
 
-
-# Following parameters are set up  dynamically by load_config() in main.py
+# Following parameters are set up dynamically by load_config() in main.py
 SOCTOOLSPROXY = None
+USER_MGMT_BASE_URL = None
 
 KEYCLOAK_BASE_URL = None
 KEYCLOAK_USERS_URL = None
diff --git a/main.py b/main.py
index 4b7b31fbcf3e4222d1039204f3a52b593d6c81a9..690adb84198be4fbe28e4e7e46d7fc9806e62892 100644
--- a/main.py
+++ b/main.py
@@ -30,6 +30,9 @@ def load_config():
     # Get FQDN of the main server
     config.SOCTOOLSPROXY = variables["soctoolsproxy"]
     assert re.match('[a-zA-Z0-9.-]+', config.SOCTOOLSPROXY), f"ERROR: The 'soctoolsproxy' variable loaded from '{config.VARIABLES_FILE}' is not a valid domain name."
+    # Set base URL for user management (this web service)
+    # TODO: load ports (or whole base URLs) from config as well
+    config.USER_MGMT_BASE_URL = f"http://{config.SOCTOOLSPROXY}:8080"
     # Set base URL to Keycloak
     config.KEYCLOAK_BASE_URL = f"https://{config.SOCTOOLSPROXY}:12443"
     config.KEYCLOAK_USERS_URL = config.KEYCLOAK_BASE_URL + "/auth/admin/realms/SOCTOOLS1/users"
@@ -82,7 +85,7 @@ class UserAccount:
             "lastName": self.lastname,
             "email": self.email,
             "attributes": {
-                "CN": [self.cn],
+                "CN": [self.cn], # TODO: should be equal to username
                 "DN": [f"CN={self.cn}"]
             },
         }
@@ -234,9 +237,6 @@ def kc_delete_user(userid: str) -> None:
         raise KeycloakError(f"({resp.status_code}) {resp.text[:200]}")
 
 
-# *** NiFi ***
-
-
 # *** Flask endpoints and forms ***
 
 class AddUserForm(FlaskForm):
@@ -305,6 +305,18 @@ def add_user():
                            lastname=form_user.lastname.data,
                            cn=form_user.cn.data,
                            dn=f"CN={form_user.cn.data}")
+
+        # Generate certificate
+        try:
+            certificates.generate_certificate(user.cn)
+            flash(f'Certificate for user "{user.username}" was successfully created.', "success")
+            #token = certificates.generate_access_token(user.cn)
+            #print(f"Certificate access token for '{user.cn}': {token}")
+            # TODO: send token to email automatically?
+        except certificates.CertError as e:
+            flash(str(e), "error")
+            return redirect("/") # don't continue creating user accounts in services
+
         # Keycloak
         try:
             kc_add_user(user)
@@ -399,6 +411,8 @@ def delete_user(username: str):
         flash(f"Error: Can't get user info from KeyCloak: {e}", "error")
         return redirect("/")
 
+    # TODO revoke certificate
+
     # Keycloak
     try:
         kc_delete_user(user_spec.kcid)
@@ -427,10 +441,10 @@ def delete_user(username: str):
     return redirect("/")
 
 
-@app.route("/export_certificate/")
+@app.route("/export_certificate")
 def export_certificate():
     """
-    Show the page allow certificate download, or provide the cert file, if "format" is given.
+    Show the page allowing certificate download, or provide the cert file, if "format" is given.
 
     Expects two parameters passed via URL:
     - "token" (mandatory) - authentication token allowing to access the certificate of the associated user.
@@ -444,44 +458,74 @@ def export_certificate():
     if not username:
         return make_response("ERROR: Invalid or expired token", 403)
 
+    user_spec = kc_get_user_by_name(username)
+    if user_spec is None:
+        flash(f"ERROR: No such user ('{username}')", "error")
+        return redirect("/")
+
     # If format is given, export and serve the certificate file
     format = request.args.get("format", None)
     if format == "p12":
         pwd = request.args.get("password", "")
-        return send_file(certificates.export_p12_certificate(username, pwd),
-                         attachment_filename=f"{username}.p12", mimetype="application/x-pkcs12")
+        return send_file(certificates.export_p12_certificate(user_spec.cn, pwd),
+                         attachment_filename=f"{user_spec.cn}.p12", mimetype="application/x-pkcs12")
     elif format == "pem-cert":
-        return send_file(certificates.get_pem_files(username)[0],
-                         attachment_filename=f"{username}.crt", mimetype="application/x-pem-file")
+        return send_file(certificates.get_pem_files(user_spec.cn)[0],
+                         attachment_filename=f"{user_spec.cn}.crt", mimetype="application/x-pem-file")
     elif format == "pem-key":
-        return send_file(certificates.get_pem_files(username)[1],
-                         attachment_filename=f"{username}.key", mimetype="application/x-pem-file")
+        return send_file(certificates.get_pem_files(user_spec.cn)[1],
+                         attachment_filename=f"{user_spec.cn}.key", mimetype="application/x-pem-file")
     # Otherwise show the HTML page
-    return render_template("export_certificate.html", username=username) # TODO
+    return render_template("export_certificate.html", token=token, username=username)
 
 
 @app.route("/send_token/<username>")
 def send_token(username: str):
-    #TODO
-    return make_response("TODO")
+    """
+    Send an email with certificate access token to the user.
+
+    """
+    # Check that the user exists
+    user_spec = kc_get_user_by_name(username)
+    if user_spec is None:
+        flash(f"ERROR: No such user ('{username}')", "error")
+        return redirect("/")
+
+    # Generate token
+    try:
+        token = certificates.generate_access_token(username)
+    except Exception as e:
+        flash(f"ERROR: {e}", "error")
+        return redirect("/")
+
+    access_url = f"{config.USER_MGMT_BASE_URL}/export_certificate?token={token}"
+    print(f"Certificate access URL for '{username}': {access_url}")
+
+    # Send the token via email
+    # TODO
+
+    flash(f"Email successfully sent to '{user_spec.email}'", "success")
+    return redirect("/")
 
 
 # TODO:
-#   (re)send cert-access token for existing user
-#   automatically create certificate when creating new user (optionally automatically send email with token)
+#   (re)send cert-access token for existing user - DONE (on click in table)
+#   automatically create certificate when creating new user (optionally automatically send email with token) - DONE
+#   revoke and delete certificate when user is deleted
+#   make CN=username (co cert filename also matches the username (it's stored by CN))
 
 
-@app.route("/test_cert/<func>")
-def test_cert_endpoint(func):
-    # run any function from "certificates" module
-    result = str(getattr(certificates, func)(**request.args))
-    return make_response(result)
+# @app.route("/test_cert/<func>")
+# def test_cert_endpoint(func):
+#     # run any function from "certificates" module
+#     result = str(getattr(certificates, func)(**request.args))
+#     return make_response(result)
 
 
 # TODO other services (besides Keycloak)
 #  - NiFi - DONE
 #  - MISP - DONE
-#  - Kibana?
+#  - Kibana? - account doesn't need to be added, but it needs to add privileges
 #  - TheHive + Cortex
 
 # TODO authentication/authorization to this GUI
diff --git a/templates/export_certificate.html b/templates/export_certificate.html
new file mode 100644
index 0000000000000000000000000000000000000000..3173ef1706b13b6345b375f186c0f3a18c75cffa
--- /dev/null
+++ b/templates/export_certificate.html
@@ -0,0 +1,14 @@
+{% extends "base.html" %}
+{% block body %}
+
+<p>The certificate for user '{{ username }}', which allows to access various SOCtools services,
+can be downloaded in the following formats:</p>
+
+{# TODO password field/prompt #}
+<p><a href="{{ url_for('export_certificate') }}?token={{token}}&amp;format=p12">PKCS12 (.p12)</a> - contains both certificate and matching private key &nbsp; <span style="font-style: italics; color: #777;">← You probably need this to import into your browser</span></p>
+
+<p><a href="{{ url_for('export_certificate') }}?token={{token}}&amp;format=pem-cert">PEM (certificate) (.crt)</a></p>
+
+<p><a href="{{ url_for('export_certificate') }}?token={{token}}&amp;format=pem-key">PEM (private key) (.key)</a></p>
+
+{% endblock %}
\ No newline at end of file