Step-By-Step Implementation Walk Through:

Step 2: Create Flask Web Application for Medication Refill Form Submission

Before you begin, ensure you have the following prerequisites and technical requirements in place to successfully implement the Flask server.

  1. Install Visual Studio Code: https://code.visualstudio.com/

Detailed Step-by-Step Implementation Guide

  1. Create a new directory named "RefillApp" in your C:\ drive by running the command: "mkdir RefillApp" and press enter

  2. Open Visual Studio Code, click File, and select Open Folder.

  1. Select the folder “RefillApp“.

  1. Select "New Terminal" from the Menu.

  1. Type the following commands in your terminal:

PS C:\RefillApp>python -m venv .venv and press enter

PS C:\RefillApp>.venv\Scripts\activate (For Windows) or source .venv\bin\activate (For Mac) and press enter

(.venv) PS C:\RefillApp> pip install flask requests python-dotenv PyJWT and press enter

  1. Create a new file called "app.py" and paste the following Python code into Visual Studio Code.

# Medication Refill App (app.py)

from flask import Flask, render_template, request, redirect, session
import requests
import os
import urllib.parse
import jwt
from dotenv import load_dotenv

# Load environment variables
load_dotenv(override=True)

app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY")

# === CONFIGURATION ===
TENANT_ID = os.getenv("TENANT_ID")
TENANT_NAME = os.getenv("TENANT_NAME")
POLICY_NAME = os.getenv("POLICY_NAME")
CLIENT_ID = os.getenv("CLIENT_ID")
CLIENT_SECRET = os.getenv("CLIENT_SECRET")
REDIRECT_URI = os.getenv("REDIRECT_URI")

SCOPE = "openid https://graph.microsoft.com/.default"
NONCE = "wpk9pP9XdI"
N8N_WEBHOOK_URL = "http://localhost:5678/webhook-test/medication-refill"

# === ROUTES ===

@app.route("/")
def index():
    return (
        "<h1>✅ It works!</h1>"
        "<p><a href='/login'>Sign in</a></p>"
    )

@app.route("/login")
def login():
    authorize_url = (
        f"https://{TENANT_NAME}.ciamlogin.com/"
        f"{TENANT_NAME}.onmicrosoft.com/oauth2/v2.0/authorize?"
        f"client_id={CLIENT_ID}"
        f"&response_type=code"
        f"&redirect_uri={urllib.parse.quote(REDIRECT_URI)}"
        f"&response_mode=query"
        f"&scope=openid"
        f"&nonce={NONCE}"
        f"&prompt=login"
    )
    return redirect(authorize_url)

@app.route("/logout")
def logout():
    # Clear the session
    session.clear()

    # Microsoft logout URL with a safer redirect
    ms_logout_url = (
        f"https://{TENANT_NAME}.ciamlogin.com/"
        f"{TENANT_NAME}.onmicrosoft.com/oauth2/v2.0/logout"
        f"?post_logout_redirect_uri={urllib.parse.quote('http://localhost:5000/')}"
    )

    return redirect(ms_logout_url)


@app.route("/auth/callback")
def auth_callback():
    code = request.args.get("code")
    if not code:
        return "❌ No code in request."

    token_url = (
        f"https://{TENANT_NAME}.ciamlogin.com/"
        f"{TENANT_ID}/oauth2/v2.0/token?p={POLICY_NAME}"
    )

    data = {
        "grant_type": "authorization_code",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "scope": SCOPE,
        "code": code,
        "redirect_uri": REDIRECT_URI,
    }

    res = requests.post(token_url, data=data)

    if res.status_code != 200:
        return f"<h1>❌ Token request failed: {res.status_code}</h1><pre>{res.text}</pre>"

    tokens = res.json()
    session["id_token"] = tokens.get("id_token")
    session["access_token"] = tokens.get("access_token")

    # === Fetch user info from Microsoft Graph ===
    headers = {"Authorization": f"Bearer {session['access_token']}"}
    graph_res = requests.get("https://graph.microsoft.com/v1.0/me", headers=headers)

    if graph_res.status_code != 200:
        return f"<h1>❌ Graph API call failed</h1><pre>{graph_res.text}</pre>"

    user_profile = graph_res.json()
    session["user"] = {
        "name": user_profile.get("displayName"),
        "email": user_profile.get("mail") or user_profile.get("userPrincipalName"),
        "oid": user_profile.get("id")
    }

    return redirect("/form")


@app.route("/form", methods=["GET", "POST"])
def form():
    if request.method == "POST":
        # handle the submitted form here
        dob = request.form.get("dob")
        hn = request.form.get("hn")
        medication = request.form.get("medication")

        # Add user info from session
        user = session.get("user", {})
        payload = {
            "dob": dob,
            "hn": hn,
            "medication": medication,
            "user": user
        }

        try:
            r = requests.post(N8N_WEBHOOK_URL, json=payload)
            if r.status_code == 200:
                return "✅ Refill request submitted!"
            else:
                return f"❌ Submission failed: {r.text}"
        except Exception as e:
            return f"❌ Error: {e}"

    # GET method (initial form display)
    user = session.get("user", {})
    return render_template("form.html", user=user)


@app.route("/submit", methods=["POST"])
def submit():
    if "user" not in session:
        return redirect("/login")

    dob = request.form.get("dob")
    hn = request.form.get("hn")
    medication = request.form.get("medication")

    payload = {
        "dob": dob,
        "hn": hn,
        "medication": medication,
        "userId": session["user"].get("oid")
    }

    try:
        r = requests.post(N8N_WEBHOOK_URL, json=payload)
        if r.status_code == 200:
            return f"✅ Refill submitted! You’ll receive confirmation soon."
        else:
            return f"❌ Something went wrong: {r.text}"
    except Exception as e:
        return f"❌ Error: {e}"

if __name__ == "__main__":
    app.run(port=5000, debug=True)
  1. Create a new directory named "templates" in the RefillApp directory and create a new file called "form.html" in the "templates" folder and paste the following HTML code into Visual Studio Code.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Refill Form</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background-color: #f8f9fa;
            color: #333;
            line-height: 1.6;
        }

        .container {
            max-width: 400px;
            margin: 80px auto;
            padding: 40px;
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }

        .icon {
            width: 40px;
            height: 40px;
            background-color: #333;
            border-radius: 6px;
            margin-bottom: 24px;
            position: relative;
        }

        .icon::before {
            content: "📋";
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 20px;
        }

        h1 {
            font-size: 32px;
            font-weight: 600;
            margin-bottom: 8px;
            color: #1a1a1a;
        }

        .subtitle {
            color: #666;
            margin-bottom: 32px;
            font-size: 16px;
        }

        .welcome {
            background-color: #f0f8ff;
            padding: 16px;
            border-radius: 6px;
            margin-bottom: 24px;
            border-left: 4px solid #1976d2;
        }

        .welcome-text {
            font-weight: 500;
            color: #1976d2;
        }

        form {
            display: flex;
            flex-direction: column;
            gap: 20px;
        }

        .form-group {
            display: flex;
            flex-direction: column;
        }

        label {
            font-weight: 500;
            margin-bottom: 8px;
            color: #333;
        }

        input[type="date"],
        input[type="text"] {
            padding: 12px 16px;
            border: 2px solid #e0e0e0;
            border-radius: 6px;
            font-size: 16px;
            transition: border-color 0.3s ease;
            background-color: white;
        }

        input[type="date"]:focus,
        input[type="text"]:focus {
            outline: none;
            border-color: #1976d2;
            box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
        }

        .submit-btn {
            background-color: #1976d2;
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 6px;
            font-size: 16px;
            font-weight: 500;
            cursor: pointer;
            transition: background-color 0.3s ease;
            margin-top: 8px;
        }

        .submit-btn:hover {
            background-color: #1565c0;
        }

        .submit-btn:active {
            transform: translateY(1px);
        }

        .logout-link {
            display: inline-flex;
            align-items: center;
            gap: 8px;
            color: #666;
            text-decoration: none;
            margin-top: 24px;
            padding: 8px 0;
            font-size: 14px;
            transition: color 0.3s ease;
        }

        .logout-link:hover {
            color: #1976d2;
        }

        @media (max-width: 480px) {
            .container {
                margin: 40px 20px;
                padding: 24px;
            }

            h1 {
                font-size: 28px;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="icon"></div>
        
        <h1>Refill Request</h1>
        <p class="subtitle">Submit your medication refill request</p>
        
        <div class="welcome">
            <div class="welcome-text">Welcome, {{ user.name }} ({{ user.email }})</div>
        </div>

        <form method="post">
            <div class="form-group">
                <label for="dob">Date of Birth</label>
                <input type="date" id="dob" name="dob" required>
            </div>

            <div class="form-group">
                <label for="hn">Hospital Number</label>
                <input type="text" id="hn" name="hn" required placeholder="Ex: HNxxxxx">
            </div>

            <div class="form-group">
                <label for="medication">Medication</label>
                <input type="text" id="medication" name="medication" required placeholder="Ex: Paracetamol 500mg">
            </div>

            <button type="submit" class="submit-btn">Submit Refill Request</button>
        </form>

        <a href="/logout" class="logout-link">
            🚪 Logout
        </a>
    </div>
</body>
</html>
  1. Create a new file called ".env" and paste the following authentication credentials and endpoint configurations from Step 1 into Visual Studio Code.

Example Authentication Credentials and Endpoint Configuration (replace with your own credentials and endpoint)

FLASK_SECRET_KEY=FLASK_SECRET_KEY=uB3K2Y!%4fD$N9sXo3tE7qP@z6W
TENANT_ID=9a674bb7-9373-4714-bc7e-982a642fee53
TENANT_NAME=aiproductivityinsights
POLICY_NAME=SignUpSignIn
CLIENT_ID=877afb01-3e95-4a86-96ee-368184eb023c
CLIENT_SECRET=iJU8Q~ahUq2HSu.PiDZ8LHzllvb~C0FmUAF
REDIRECT_URI=http://localhost:5000/auth/callback
  1. Make sure to save all the files that you have created.

Creating an Environment File: Essential Setup for Secure Configuration

The .env file and app.py file have a direct correlation where .env stores configuration variables that app.py loads and uses throughout the application. This pattern separates sensitive credentials from code while maintaining application functionality.

1. Loading the .env file

In app.py:

from dotenv import load_dotenv
load_dotenv(override=True)

This loads all the key-value pairs in your .env file into environment variables so that os.getenv() can fetch them.

2. Secret Key for Flask Sessions

  • .env:

    FLASK_SECRET_KEY=uB3K2Y!%4fD$N9sXo3tE7qP@z6W
    
    
  • app.py:

    app.secret_key = os.getenv("FLASK_SECRET_KEY")
    
    

🔑 Used to encrypt Flask session cookies so user sessions (e.g., after login) are secure.

3. Microsoft Entra (Azure AD B2C / External ID) Settings

  • .env provides:

    TENANT_ID=9a674bb7-9373-4714-bc7e-982a642fee53
    TENANT_NAME=aiproductivityinsights
    POLICY_NAME=SignUpSignIn
    CLIENT_ID=877afb01-3e95-4a86-96ee-368184eb023c
    CLIENT_SECRET=iJU8Q~ahUq2HSu.PiDZ8LHzllvb~C0FmUAF
    REDIRECT_URI=http://localhost:5000/auth/callback
    
    
  • app.py maps them:

    TENANT_ID = os.getenv("TENANT_ID")
    TENANT_NAME = os.getenv("TENANT_NAME")
    POLICY_NAME = os.getenv("POLICY_NAME")
    CLIENT_ID = os.getenv("CLIENT_ID")
    CLIENT_SECRET = os.getenv("CLIENT_SECRET")
    REDIRECT_URI = os.getenv("REDIRECT_URI")
    
    

Where they are used:

  • Login URL (authorization request):

    authorize_url = (
        f"https://{TENANT_NAME}.ciamlogin.com/"
        f"{TENANT_NAME}.onmicrosoft.com/oauth2/v2.0/authorize?"
        f"client_id={CLIENT_ID}"
        f"&response_type=code"
        f"&redirect_uri={urllib.parse.quote(REDIRECT_URI)}"
        f"&scope=openid"
        f"&nonce={NONCE}"
    )
    
    

    👉 This sends the user to Microsoft login with your tenant name, client ID, and redirect URI.

  • Token request (exchange authorization code for tokens):

    token_url = (
        f"https://{TENANT_NAME}.ciamlogin.com/"
        f"{TENANT_ID}/oauth2/v2.0/token?p={POLICY_NAME}"
    )
    
    data = {
        "grant_type": "authorization_code",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "scope": SCOPE,
        "code": code,
        "redirect_uri": REDIRECT_URI,
    }
    res = requests.post(token_url, data=data)
    
    

    👉 This uses CLIENT_ID, CLIENT_SECRET, TENANT_ID, and POLICY_NAME to securely request tokens.

4. Redirect URI

  • .env:

    REDIRECT_URI=http://localhost:5000/auth/callback
    
    
  • app.py:

    @app.route("/auth/callback")
    
    

    👉 After login, Microsoft redirects the browser here with a code, which your app exchanges for tokens.

5. How it all fits together

  1. User visits /login → redirected to Microsoft login using your CLIENT_ID and TENANT_NAME.

  2. Microsoft validates credentials & policy (SignUpSignIn).

  3. Microsoft redirects back to REDIRECT_URI with a code.

  4. Flask /auth/callback exchanges that code for tokens using TENANT_ID, CLIENT_ID, CLIENT_SECRET.

  5. Flask stores tokens in session (protected by FLASK_SECRET_KEY).

  6. Authenticated user can now submit the refill form (form.html), and the data goes to n8n for processing.

In Short:

  • .env = secure storage of sensitive configuration.

  • app.py = consumes these variables to build OAuth flows with Microsoft Entra ID and keep sessions safe.