English Deutsch
preview
MQL5で取引管理者パネルを作成する(第5回):2要素認証(2FA)

MQL5で取引管理者パネルを作成する(第5回):2要素認証(2FA)

MetaTrader 5 | 22 1月 2025, 14:49
184 0
Clemence Benjamin
Clemence Benjamin

内容


はじめに

以前、管理者とトレーダー間の通信を保護するための重要な第一歩として、管理パネル内でのパスコード認証の実装について検討しました。この基本的なセキュリティ形式は初期の保護には不可欠ですが、単一の検証要素に依存しているため、システムが潜在的な脅威にさらされる可能性があります。2要素認証(2FA)の組み込みは重要なアップグレードとなり、アプリケーションに強固なセキュリティフレームワークを提供します。

このアプローチでは、パスワードの知識に加えて二次的な認証方法へのアクセスも求められるため、不正アクセスのリスクが大幅に軽減されます。2FAは、すべての通信が正当なソースから発信されることを保証し、データ侵害や誤った情報、市場操作などの潜在的な影響から保護します。

2FAを管理パネルに統合することで、ユーザーにより高いレベルの信頼とセキュリティを提供できます。この二重認証システムは、潜在的な違反に対する強力な抑止力となり、管理者とトレーダーに、動的な金融環境で安心して業務をおこなえる環境を提供します。今日は、私が実際に機能するソリューションに変換したコンセプトについて説明します。この取り組みにより、前回の記事で説明したパスコードセキュリティに関連する脆弱性に対応し、管理パネルのセキュリティが強化されました。


2要素認証とは

2要素認証(2FA)は、アカウントやシステムへのアクセスを許可する前に、2つの異なる形式の認証を要求するセキュリティメカニズムです。これは、2つ以上の認証要素を使用する多要素認証(MFA)の一部といえます。2FAの主な目的は、ユーザー名やパスワードといった従来のセキュリティにさらなる層を追加し、不正アクセスをより困難にすることです。

一般的に、2FAでは以下の3つのカテゴリの認証要素のうち2つが使用されます。

  1. 知っているもの(知識要素):ユーザーがアカウントにアクセスするために入力するパスワードやPINなどが該当します。最初の防御ラインとして機能します。
  2. 持っているもの(所有要素):セキュリティトークン、スマートカード、携帯電話など、ユーザーが物理的に所有しているデバイスやトークンを指します。多くのシステムでは、Google AuthenticatorやAuthyといったアプリを利用して、短期間(通常は15分間)有効なワンタイムパスワード(OTP)を生成します。
  3. 本人そのもの(生体認証要素):指紋認証や顔認証などの生体認証データがこれに該当します。生体認証は、2FAの最初の2つの要素と組み合わせて使用されることもあれば、追加のセキュリティ層として活用されることもあります。


MQL5を使用して管理パネルに2要素認証(2FA)を実装する

2FAを統合することで、従来のユーザー名とパスワードのみに依存した単一要素認証(SFA)から一歩進んだセキュリティを実現できます。この変革は極めて重要です。なぜなら、パスワードは依然として最も一般的な初期セキュリティ手段であるものの、ソーシャルエンジニアリング、ブルートフォース攻撃、辞書攻撃といったさまざまな脅威に対して本質的に脆弱だからです。2FAの多要素認証アプローチは、異なるカテゴリーに属する2つの独立した認証要素を要求することで、これらのリスクを効果的に軽減します。これにより、アクセスが正当なユーザーにのみ許可されるという信頼性が大幅に向上します。

管理パネルプロジェクトに2FAを実装するにあたり、Dialogライブラリを活用しました。このライブラリを使用することで、特定のロジックによって制御される複数のウィンドウレイヤーを作成できます。この連載の初期段階では、Telegram通信を統合し、主に管理パネルからTelegram上のユーザーチャンネルやグループにメッセージを送信することに焦点を当てていました。しかし、その活用範囲はそれだけに留まりません。今回はTelegramを利用してOTPの配信を行う仕組みを構築することを目指します。

本プロジェクトでは、ランダムな6桁のコードを生成するようにコードを調整します。このコードはプログラム内で安全に保存され、その後、管理者のTelegramアカウントに送信されて検証に使用されます。多くの企業がTelegramを検証目的で使用していることに気づきましたが、その実装方法は様々です。例えば、以下の画像に示されているMQL5検証ボットは、このような使用例の1つです。

MQL5検証ボット

MQL5検証ボット


パスワード入力と2FAコード検証用のGUI要素の実装

パスワード入力ダイアログを実装する際は、まずパスワード入力用のユーザーインターフェイスを作成するために必要なすべての手順をカプセル化するShowAuthenticationPrompt()関数を定義します。このプロセスは、Create()メソッドを使用して認証ダイアログをインスタンス化し、その寸法とチャート上の位置を指定することから始まります。次に、ユーザーのパスワードを安全に取得するためのpasswordInputBoxを作成します。続いて、ダイアログの目的をユーザーに案内するために、明確な指示を提供するpasswordPromptLabelを追加します。さらに、ユーザーからのフィードバック、特に誤った入力を処理するためのfeedbackLabelを実装します。このラベルにはエラーメッセージが赤いテキストで表示され、ユーザーが何が問題だったのかを容易に理解できるようにします。次に、以下の2つのボタンを設定します。

  • loginButton:ユーザーがクリックして認証のためにパスワードを送信するためのボタン 
  • closeAuthButton:ユーザーが続行しない場合にダイアログを終了するためのボタン 

最後に、認証ダイアログに対してshow()を呼び出し、ユーザーに表示します。そしてChartRedraw()を呼び出して、すべてのコンポーネントが画面上に正しくレンダリングされることを保証します。この体系的なアプローチにより、管理パネルでのパスワード入力が安全かつユーザーフレンドリーなインターフェイスで提供されます。次のコードスニペットを参照してください。

パスワード入力ダイアログの作成

// Show authentication input dialog
bool ShowAuthenticationPrompt()
{
    if (!authentication.Create(ChartID(), "Authentication", 0, 100, 100, 500, 300))
    {
        Print("Failed to create authentication dialog");
        return false;
    }

    // Create password input box
    if (!passwordInputBox.Create(ChartID(), "PasswordInputBox", 0, 20, 70, 260, 95))
    {
        Print("Failed to create password input box");
        return false;
    }
    authentication.Add(passwordInputBox);

    // Create password prompt label
    if (!passwordPromptLabel.Create(ChartID(), "PasswordPromptLabel", 0, 20, 20, 260, 40))
    {
        Print("Failed to create password prompt label");
        return false;
    }
    passwordPromptLabel.Text("Enter password: Access Admin Panel");
    authentication.Add(passwordPromptLabel);

    // Create feedback label for wrong attempts
    if (!feedbackLabel.Create(ChartID(), "FeedbackLabel", 0, 20, 140, 380, 40))
    {
        Print("Failed to create feedback label");
        return false;
    }
    feedbackLabel.Text("");
    feedbackLabel.Color(clrRed); // Set color for feedback
    authentication.Add(feedbackLabel);

    // Create login button
    if (!loginButton.Create(ChartID(), "LoginButton", 0, 20, 120, 100, 40))
    {
        Print("Failed to create login button");
        return false;
    }
    loginButton.Text("Login");
    authentication.Add(loginButton);

    // Create close button for authentication dialog
    if (!closeAuthButton.Create(ChartID(), "CloseAuthButton", 0, 120, 120, 200, 40))
    {
        Print("Failed to create close button for authentication");
        return false;
    }
    closeAuthButton.Text("Close");
    authentication.Add(closeAuthButton);

    authentication.Show();
    ChartRedraw();
    return true;
}


2FAコード検証ダイアログの作成

2要素認証(2FA)コードの検証に必要なすべてのコンポーネントを処理するShowTwoFactorAuthPrompt()関数を定義します。この関数により、ユーザーが受け取った2FAコードを確認するためのダイアログを作成します。

最初に、Create()メソッドを使用してプロパティを設定し、twoFactorAuthダイアログを作成します。最初に追加されるコンポーネントはtwoFACodeInputで、これはユーザーのTelegramに送信された2FAコードを安全に入力するためのフィールドとして設計されています。

ユーザーを適切にガイドするために、twoFAPromptLabelを実装します。このラベルは、受け取った2FAコードを入力するように明確な指示を提供します。さらに、ユーザーエクスペリエンスを向上させるために、リアルタイムのフィードバックを表示するtwoFAFeedbackLabelを追加します。このラベルには、入力されたコードが期待値と一致しない場合、赤いメッセージが表示され、ユーザーに誤った入力が通知されます。

コードの送信をおこなうためにtwoFALoginButtonを作成し、ユーザーが入力したコードを確認できるようにします。また、必要に応じてダイアログを終了するためのclose2FAButtonも提供します。すべてのコンポーネントが設定された後、Show()を呼び出してtwoFactorAuthダイアログをユーザーに表示し、続けてChartRedraw()を呼び出してインターフェイスを更新します。

この構造化されたアプローチにより、2FAコードを安全に検証できる方法と、管理パネルでのユーザー操作を合理化する仕組みが実現します。以下に、理解を深めるためのコードスニペットを示します。

// Show two-factor authentication input dialog
void ShowTwoFactorAuthPrompt()
{
    if (!twoFactorAuth.Create(ChartID(), "Two-Factor Authentication", 0, 100, 100, 500, 300))
    {
        Print("Failed to create 2FA dialog");
        return;
    }

    // Create input box for 2FA code
    if (!twoFACodeInput.Create(ChartID(), "TwoFACodeInput", 0, 20, 70, 260, 95))
    {
        Print("Failed to create 2FA code input box");
        return;
    }
    twoFactorAuth.Add(twoFACodeInput);

    // Create prompt label for 2FA
    if (!twoFAPromptLabel.Create(ChartID(), "TwoFAPromptLabel", 0, 20, 20, 380, 40))
    {
        Print("Failed to create 2FA prompt label");
        return;
    }
    twoFAPromptLabel.Text("Enter the 2FA code sent to your Telegram:");
    twoFactorAuth.Add(twoFAPromptLabel);

    // Create feedback label for wrong attempts
    if (!twoFAFeedbackLabel.Create(ChartID(), "TwoFAFeedbackLabel", 0, 20, 140, 380, 40))
    {
        Print("Failed to create 2FA feedback label");
        return;
    }
    twoFAFeedbackLabel.Text("");
    twoFAFeedbackLabel.Color(clrRed); // Set color for feedback
    twoFactorAuth.Add(twoFAFeedbackLabel);

    // Create login button for 2FA code submission
    if (!twoFALoginButton.Create(ChartID(), "TwoFALoginButton", 0, 20, 120, 100, 40))
    {
        Print("Failed to create 2FA login button");
        return;
    }
    twoFALoginButton.Text("Verify");
    twoFactorAuth.Add(twoFALoginButton);

    // Create close button for 2FA dialog
    if (!close2FAButton.Create(ChartID(), "Close2FAButton", 0, 120, 120, 200, 40))
    {
        Print("Failed to create close button for 2FA");
        return;
    }
    close2FAButton.Text("Close");
    twoFactorAuth.Add(close2FAButton);

    twoFactorAuth.Show();
    ChartRedraw();
}


検証コード生成アルゴリズム

管理パネルへのアクセス用パスワードを正常に入力した後、OTPコード生成を含む第2の保護層が確立されます。このコードは安全に保存され、モバイルまたはデスクトップ上のTelegramアプリにリンクされた特定のハードコードされたチャットIDに転送されます。正当な所有者はこのコードを取得し、プロンプトに入力することで、認証を進めることができます。入力されたコードが一致すると、アプリケーションは管理パネルのすべての機能へのアクセスを許可します。このアクセスにより、操作および通信が可能となります。

コード生成アルゴリズムに関しては、以下のコードスニペットのさまざまなコンポーネントについて説明します。

以下のコード行は、プログラム内で2要素認証(2FA)を管理するための重要な変数宣言として機能します。

string twoFACode = "";

この行は、変数twoFACodeを空の文字列として初期化します。この変数はランダムに生成される6桁の2要素認証コードを格納するために使用されます。認証プロセス全体を通じて、この変数は、ユーザーが管理パネルにアクセスするために正しいパスワードを正常に入力した後にTelegram経由でユーザーに送信される実際のコードを保持するという重要な役割を果たします。

ユーザーが最初のパスワードチェックに合格すると、6桁の数字文字列を生成するGenerateRandom6DigitCode()関数によって生成された新しい値がtwoFACode変数に設定されます。その後、SendMessageToTelegram()を使用して、この値がユーザーのTelegramに送信されます。

次に、ユーザーに2FAコードの入力を求めるプロンプトが表示されると、プログラムはユーザーの入力をtwoFACodeに保存されている値と比較します。ユーザーの入力がtwoFACodeに保持されている値と一致する場合、管理パネルへのアクセスが許可されます。一致しない場合は、エラーメッセージが表示されます。

1. パスワード認証

string Password = "2024"; // Hardcoded password

// Handle login button click
void OnLoginButtonClick()
{
    string enteredPassword = passwordInputBox.Text();
    if (enteredPassword == Password)
    {
        twoFACode = GenerateRandom6DigitCode();
        SendMessageToTelegram("Your 2FA code is: " + twoFACode, Hardcoded2FAChatId, Hardcoded2FABotToken);
        authentication.Destroy();
        ShowTwoFactorAuthPrompt();
        Print("Password authentication successful. A 2FA code has been sent to your Telegram.");
    }
    else
    {
        feedbackLabel.Text("Wrong password. Try again.");
        passwordInputBox.Text("");
    }
}

ハードコードされたパスワード(「2024」)は、管理パネルのアクセス制御メカニズムとして機能します。ユーザーが指定された入力ボックスから入力したパスワードを送信すると、コードは入力がハードコードされたパスワードと一致するかどうかを確認します。一致すると、GenerateRandom6DigitCode()関数を使用してランダムな6桁のコードが生成され、SendMessageToTelegram()関数を介してユーザーのTelegramに送信されます。これは認証が成功したことを示し、アプリケーションが2要素認証フェーズに移行するように促します。パスワードが間違っている場合は、エラーメッセージが表示され、ユーザーに再試行するよう促します。

2. 2要素認証コードの生成と配信

// Generate a random 6-digit code for 2FA
string GenerateRandom6DigitCode()
{
    int code = MathRand() % 1000000; // Produces a 6-digit number
    return StringFormat("%06d", code); // Ensures leading zeros
}

// Handle 2FA login button click
void OnTwoFALoginButtonClick()
{
    string enteredCode = twoFACodeInput.Text();
    if (enteredCode == twoFACode)
    {
        twoFactorAuth.Destroy();
        adminPanel.Show();
        Print("2FA authentication successful.");
    }
    else
    {
        twoFAFeedbackLabel.Text("Wrong code. Try again.");
        twoFACodeInput.Text("");
    }
}

このセクションでは、一意の6桁コードを生成し、ユーザー入力をそのコードと照合することで、2要素認証(2FA)を管理します。GenerateRandom6DigitCode()関数は、MathRand()関数を使用して6桁の数字を生成し、先頭にゼロが含まれる場合でも必要な形式を維持します。最初のパスワードが検証されると、この6桁コードがTelegramを通じてユーザーが指定したチャットに送信され、セキュリティが強化されます。次に、OnTwoFALoginButtonClick()の関数で、ユーザーの入力が生成されたコードと比較されます。入力したコードがTelegramで送信されたコードと一致する場合、管理パネルへのアクセスが許可されます。一致しない場合は、ユーザーにコードが誤っている旨が通知され、再入力を求められます。


MathRand()関数について

MQL5におけるこの関数は、疑似乱数の整数を生成するために使用されます。このプロジェクトでは、MathRand()関数は0からMathRandMax()の範囲内でランダムな整数を生成します。これは通常0から32767と定義されます。 

ランダムな6桁の数字を生成するには、モジュロ演算子(%)を適用して出力範囲を制限します。この演算子は割り算の余りを計算し、乱数の範囲を000000 (0)から999999までの有効な6桁の値に制限することができます。

具体的には、MathRand() % 1000000という式を使用することで、0から999999の間の結果が生成されます。これにより、先頭にゼロが付くものを含む、すべての6桁の組み合わせを網羅することが保証されます。


2FAのためのTelegram API

SendMessageToTelegram()関数は、2FAコードやその他のメッセージをユーザーのTelegramチャットに安全に配信するために重要な役割を果たします。この関数では、ボットトークンと宛先チャットIDを含むHTTP POSTリクエストを構築し、Telegram Bot APIに送信します。メッセージは、APIの要件を満たすためにJSON形式でフォーマットされます。

WebRequest()関数は、指定されたタイムアウト値でリクエストを実行し、応答コードを確認してメッセージが正常に送信されたか(HTTPコード200)を判断します。

もしメッセージの配信が失敗した場合、この関数は応答コード、エラーメッセージ、および関連する応答内容を含むエラー情報をログに記録します。これにより、メッセージ送信時の潜在的な問題を特定し、さらなる診断が可能になります。

Telegram APIの詳細については、公式Webサイトをご覧いただくか、以前に説明した関連する記事をお読みください。

bool SendMessageToTelegram(string message, string chatId, string botToken)
{
    string url = "https://api.telegram.org/bot" + botToken + "/sendMessage";
    string jsonMessage = "{\"chat_id\":\"" + chatId + "\", \"text\":\"" + message + "\"}";

    char postData[];
    ArrayResize(postData, StringToCharArray(jsonMessage, postData) - 1);

    int timeout = 5000;
    char result[];
    string responseHeaders;
    int responseCode = WebRequest("POST", url, "Content-Type: application/json\r\n", timeout, postData, result, responseHeaders);

    if (responseCode == 200)
    {
        Print("Message sent successfully: ", message);
        return true;
    }
    else
    {
        Print("Failed to send message. HTTP code: ", responseCode, " Error code: ", GetLastError());
        Print("Response: ", CharArrayToString(result));
        return false;
    }
}

Telegram APIの一部として、Telegramを利用した2要素認証(2FA)プロセスを簡略化するために、プロジェクトに組み込んだ以下の定数があります。 

// Constants for 2FA
const string Hardcoded2FAChatId = "REPLACE WITH YOUR CHAT ID";
const string Hardcoded2FABotToken = "REPLACE WITH YOUR ACTUAL BOT TOKEN";

このコードの一部では、変数Hardcoded2FAChatIdは認証メッセージが送信されるTelegramチャットの一意のチャット識別子を表し、Hardcoded2FABotTokenはメッセージ送信に使用されるTelegramボットのトークンを保持します。

ボットトークンはTelegram APIへのリクエストを認証するために不可欠であり、正しい権限を持つ正当なボットのみが指定されたチャットにメッセージを送信できるようにします。これらの定数をハードコーディングすることで、ユーザーの入力や設定を必要とせず、毎回同じチャットIDとボットトークンが使用されるため、2FAコードの送信プロセスを効率化できます。

しかし、ボットトークンのような機密情報をハードコーディングすることは、コードが公開された場合にセキュリティリスクを伴う可能性があるため、本番環境では代替となる安全な保存方法を検討する必要があります。

コード全体を見てみると、新しい機能が実装されるにつれて、コードが大幅に拡張されていることが確認できます。

//+------------------------------------------------------------------+
//|                                             Admin Panel.mq5      |
//|                           Copyright 2024, Clemence Benjamin      |
//|        https://www.mql5.com/ja/users/billionaire2024/seller      |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, Clemence Benjamin"
#property link      "https://www.mql5.com/ja/users/billionaire2024/seller"
#property description "A secure and  responsive Admin Panel. Send messages to your telegram clients without leaving MT5"
#property version   "1.20"

#include <Trade\Trade.mqh>
#include <Controls\Dialog.mqh>
#include <Controls\Button.mqh>
#include <Controls\Edit.mqh>
#include <Controls\Label.mqh>

// Input parameters for quick messages
input string QuickMessage1 = "Updates";
input string QuickMessage2 = "Close all";
input string QuickMessage3 = "In deep profits";
input string QuickMessage4 = "Hold position";
input string QuickMessage5 = "Swing Entry";
input string QuickMessage6 = "Scalp Entry";
input string QuickMessage7 = "Book profit";
input string QuickMessage8 = "Invalid Signal";
input string InputChatId = "YOUR_CHAT_ID";
input string InputBotToken = "YOUR_BOT_TOKEN";

// Constants for 2FA
const string Hardcoded2FAChatId = "ENTER YOUR REAL CHAT ID";
const string Hardcoded2FABotToken = "ENTER YOUR Telegram Bot Token";

// Global variables
CDialog adminPanel;
CDialog authentication, twoFactorAuth;
CButton sendButton, clearButton, changeFontButton, toggleThemeButton;
CButton loginButton, closeAuthButton, twoFALoginButton, close2FAButton;
CButton quickMessageButtons[8], minimizeButton, maximizeButton, closeButton;
CEdit inputBox, passwordInputBox, twoFACodeInput;
CLabel charCounter, passwordPromptLabel, feedbackLabel, twoFAPromptLabel, twoFAFeedbackLabel;
bool minimized = false;
bool darkTheme = false;
int MAX_MESSAGE_LENGTH = 4096;
string availableFonts[] = { "Arial", "Courier New", "Verdana", "Times New Roman" };
int currentFontIndex = 0;
string Password = "2024"; // Hardcoded password
string twoFACode = "";

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    if (!ShowAuthenticationPrompt())
    {
        Print("Authorization failed. Exiting...");
        return INIT_FAILED;
    }

    if (!adminPanel.Create(ChartID(), "Admin Panel", 0, 30, 30, 500, 500))
    {
        Print("Failed to create admin panel dialog");
        return INIT_FAILED;
    }

    if (!CreateControls())
    {
        Print("Control creation failed");
        return INIT_FAILED;
    }

    adminPanel.Hide();
    Print("Initialization complete");
    return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Show authentication input dialog                                 |
//+------------------------------------------------------------------+
bool ShowAuthenticationPrompt()
{
    if (!authentication.Create(ChartID(), "Authentication", 0, 100, 100, 500, 300))
    {
        Print("Failed to create authentication dialog");
        return false;
    }

    if (!passwordInputBox.Create(ChartID(), "PasswordInputBox", 0, 20, 70, 260, 95))
    {
        Print("Failed to create password input box");
        return false;
    }
    authentication.Add(passwordInputBox);

    if (!passwordPromptLabel.Create(ChartID(), "PasswordPromptLabel", 0, 20, 20, 260, 40))
    {
        Print("Failed to create password prompt label");
        return false;
    }
    passwordPromptLabel.Text("Enter password: Access Admin Panel");
    authentication.Add(passwordPromptLabel);

    if (!feedbackLabel.Create(ChartID(), "FeedbackLabel", 0, 20, 140, 380, 160))
    {
        Print("Failed to create feedback label");
        return false;
    }
    feedbackLabel.Text("");
    feedbackLabel.Color(clrRed); // Red color for incorrect attempts
    authentication.Add(feedbackLabel);

    if (!loginButton.Create(ChartID(), "LoginButton", 0, 20, 120, 100, 140))
    {
        Print("Failed to create login button");
        return false;
    }
    loginButton.Text("Login");
    authentication.Add(loginButton);

    if (!closeAuthButton.Create(ChartID(), "CloseAuthButton", 0, 120, 120, 200, 140))
    {
        Print("Failed to create close button for authentication");
        return false;
    }
    closeAuthButton.Text("Close");
    authentication.Add(closeAuthButton);

    authentication.Show();
    ChartRedraw();

    return true;
}

//+------------------------------------------------------------------+
//| Show two-factor authentication input dialog                      |
//+------------------------------------------------------------------+
void ShowTwoFactorAuthPrompt()
{
    if (!twoFactorAuth.Create(ChartID(), "Two-Factor Authentication", 0, 100, 100, 500, 300))
    {
        Print("Failed to create 2FA dialog");
        return;
    }

    if (!twoFACodeInput.Create(ChartID(), "TwoFACodeInput", 0, 20, 70, 260, 95))
    {
        Print("Failed to create 2FA code input box");
        return;
    }
    twoFactorAuth.Add(twoFACodeInput);

    if (!twoFAPromptLabel.Create(ChartID(), "TwoFAPromptLabel", 0, 20, 20, 380, 40))
    {
        Print("Failed to create 2FA prompt label");
        return;
    }
    twoFAPromptLabel.Text("Enter the 2FA code sent to your Telegram:");
    twoFactorAuth.Add(twoFAPromptLabel);

    if (!twoFAFeedbackLabel.Create(ChartID(), "TwoFAFeedbackLabel", 0, 20, 140, 380, 160))
    {
        Print("Failed to create 2FA feedback label");
        return;
    }
    twoFAFeedbackLabel.Text("");
    twoFAFeedbackLabel.Color(clrRed); // Red color for incorrect 2FA attempts
    twoFactorAuth.Add(twoFAFeedbackLabel);

    if (!twoFALoginButton.Create(ChartID(), "TwoFALoginButton", 0, 20, 120, 100, 140))
    {
        Print("Failed to create 2FA login button");
        return;
    }
    twoFALoginButton.Text("Verify");
    twoFactorAuth.Add(twoFALoginButton);

    if (!close2FAButton.Create(ChartID(), "Close2FAButton", 0, 120, 120, 200, 140))
    {
        Print("Failed to create close button for 2FA");
        return;
    }
    close2FAButton.Text("Close");
    twoFactorAuth.Add(close2FAButton);

    twoFactorAuth.Show();
    ChartRedraw();
}

//+------------------------------------------------------------------+
//| Handle chart events                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
    if (id == CHARTEVENT_OBJECT_CLICK)
    {
        if (sparam == "LoginButton")
        {
            OnLoginButtonClick();
        }
        else if (sparam == "CloseAuthButton")
        {
            OnCloseAuthButtonClick();
        }
        else if (sparam == "TwoFALoginButton")
        {
            OnTwoFALoginButtonClick();
        }
        else if (sparam == "Close2FAButton")
        {
            OnClose2FAButtonClick();
        }
    }

    switch (id)
    {
        case CHARTEVENT_OBJECT_CLICK:
            if (sparam == "SendButton") OnSendButtonClick();
            else if (sparam == "ClearButton") OnClearButtonClick();
            else if (sparam == "ChangeFontButton") OnChangeFontButtonClick();
            else if (sparam == "ToggleThemeButton") OnToggleThemeButtonClick();
            else if (sparam == "MinimizeButton") OnMinimizeButtonClick();
            else if (sparam == "MaximizeButton") OnMaximizeButtonClick();
            else if (sparam == "CloseButton") OnCloseButtonClick();
            else if (StringFind(sparam, "QuickMessageButton") != -1)
            {
                long index = StringToInteger(StringSubstr(sparam, 18));
                OnQuickMessageButtonClick(index - 1);
            }
            break;

        case CHARTEVENT_OBJECT_ENDEDIT:
            if (sparam == "InputBox") OnInputChange();
            break;
    }
}

//+------------------------------------------------------------------+
//| Handle login button click                                        |
//+------------------------------------------------------------------+
void OnLoginButtonClick()
{
    string enteredPassword = passwordInputBox.Text();
    if (enteredPassword == Password)
    {
        twoFACode = GenerateRandom6DigitCode();
        SendMessageToTelegram("A login attempt was made on the Admin Panel. Please use this code to verify your identity. " + twoFACode, Hardcoded2FAChatId, Hardcoded2FABotToken);

        authentication.Destroy();
        ShowTwoFactorAuthPrompt();
        Print("Password authentication successful. A 2FA code has been sent to your Telegram.");
    }
    else
    {
        feedbackLabel.Text("Wrong password. Try again.");
        passwordInputBox.Text("");
    }
}

//+------------------------------------------------------------------+
//| Handle 2FA login button click                                    |
//+------------------------------------------------------------------+
void OnTwoFALoginButtonClick()
{
    string enteredCode = twoFACodeInput.Text();
    if (enteredCode == twoFACode)
    {
        twoFactorAuth.Destroy();
        adminPanel.Show();
        Print("2FA authentication successful.");
    }
    else
    {
        twoFAFeedbackLabel.Text("Wrong code. Try again.");
        twoFACodeInput.Text("");
    }
}

//+------------------------------------------------------------------+
//| Handle close button for authentication                           |
//+------------------------------------------------------------------+
void OnCloseAuthButtonClick()
{
    authentication.Destroy();
    ExpertRemove(); // Exit the expert
    Print("Authentication dialog closed.");
}

//+------------------------------------------------------------------+
//| Handle close button for 2FA                                      |
//+------------------------------------------------------------------+
void OnClose2FAButtonClick()
{
    twoFactorAuth.Destroy();
    ExpertRemove();
    Print("2FA dialog closed.");
}

//+------------------------------------------------------------------+
//| Create necessary UI controls                                     |
//+------------------------------------------------------------------+
bool CreateControls()
{
    long chart_id = ChartID();

    if (!inputBox.Create(chart_id, "InputBox", 0, 5, 25, 460, 95))
    {
        Print("Failed to create input box");
        return false;
    }
    adminPanel.Add(inputBox);

    if (!charCounter.Create(chart_id, "CharCounter", 0, 380, 5, 460, 25))
    {
        Print("Failed to create character counter");
        return false;
    }
    charCounter.Text("0/" + IntegerToString(MAX_MESSAGE_LENGTH));
    adminPanel.Add(charCounter);

    if (!clearButton.Create(chart_id, "ClearButton", 0, 235, 95, 345, 125))
    {
        Print("Failed to create clear button");
        return false;
    }
    clearButton.Text("Clear");
    adminPanel.Add(clearButton);

    if (!sendButton.Create(chart_id, "SendButton", 0, 350, 95, 460, 125))
    {
        Print("Failed to create send button");
        return false;
    }
    sendButton.Text("Send");
    adminPanel.Add(sendButton);

    if (!changeFontButton.Create(chart_id, "ChangeFontButton", 0, 95, 95, 230, 115))
    {
        Print("Failed to create change font button");
        return false;
    }
    changeFontButton.Text("Font<>");
    adminPanel.Add(changeFontButton);

    if (!toggleThemeButton.Create(chart_id, "ToggleThemeButton", 0, 5, 95, 90, 115))
    {
        Print("Failed to create toggle theme button");
        return false;
    }
    toggleThemeButton.Text("Theme<>");
    adminPanel.Add(toggleThemeButton);

    if (!minimizeButton.Create(chart_id, "MinimizeButton", 0, 375, -22, 405, 0))
    {
        Print("Failed to create minimize button");
        return false;
    }
    minimizeButton.Text("_");
    adminPanel.Add(minimizeButton);

    if (!maximizeButton.Create(chart_id, "MaximizeButton", 0, 405, -22, 435, 0))
    {
        Print("Failed to create maximize button");
        return false;
    }
    maximizeButton.Text("[ ]");
    adminPanel.Add(maximizeButton);

    if (!closeButton.Create(chart_id, "CloseButton", 0, 435, -22, 465, 0))
    {
        Print("Failed to create close button");
        return false;
    }
    closeButton.Text("X");
    adminPanel.Add(closeButton);

    return CreateQuickMessageButtons();
}

//+------------------------------------------------------------------+
//| Create quick message buttons                                     |
//+------------------------------------------------------------------+
bool CreateQuickMessageButtons()
{
    string quickMessages[] = { QuickMessage1, QuickMessage2, QuickMessage3, QuickMessage4, QuickMessage5, QuickMessage6, QuickMessage7, QuickMessage8 };
    int startX = 5, startY = 160, width = 222, height = 65, spacing = 5;

    for (int i = 0; i < ArraySize(quickMessages); i++)
    {
        bool created = quickMessageButtons[i].Create(ChartID(), "QuickMessageButton" + IntegerToString(i + 1), 0,
            startX + (i % 2) * (width + spacing), startY + (i / 2) * (height + spacing), 
            startX + (i % 2) * (width + spacing) + width, startY + (i / 2) * (height + spacing) + height);

        if (!created)
        {
            Print("Failed to create quick message button ", i + 1);
            return false;
        }
        quickMessageButtons[i].Text(quickMessages[i]);
        adminPanel.Add(quickMessageButtons[i]);
    }
    return true;
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    adminPanel.Destroy();
    Print("Deinitialization complete");
}

//+------------------------------------------------------------------+
//| Handle custom message send button click                          |
//+------------------------------------------------------------------+
void OnSendButtonClick()
{
    string message = inputBox.Text();
    if (StringLen(message) > 0)
    {
        if (SendMessageToTelegram(message, InputChatId, InputBotToken))
            Print("Custom message sent: ", message);
        else
            Print("Failed to send custom message.");
    }
    else
    {
        Print("No message entered.");
    }
}
//+------------------------------------------------------------------+
//| Handle clear button click                                        |
//+------------------------------------------------------------------+
void OnClearButtonClick()
{
    inputBox.Text("");
    OnInputChange();
    Print("Input box cleared.");
}

//+------------------------------------------------------------------+
//| Handle quick message button click                                |
//+------------------------------------------------------------------+
void OnQuickMessageButtonClick(long index)
{
    string quickMessages[] = { QuickMessage1, QuickMessage2, QuickMessage3, QuickMessage4, QuickMessage5, QuickMessage6, QuickMessage7, QuickMessage8 };
    string message = quickMessages[(int)index];

    if (SendMessageToTelegram(message, InputChatId, InputBotToken))
        Print("Quick message sent: ", message);
    else
        Print("Failed to send quick message.");
}

//+------------------------------------------------------------------+
//| Update character counter                                         |
//+------------------------------------------------------------------+
void OnInputChange()
{
    int currentLength = StringLen(inputBox.Text());
    charCounter.Text(IntegerToString(currentLength) + "/" + IntegerToString(MAX_MESSAGE_LENGTH));
    ChartRedraw();
}

//+------------------------------------------------------------------+
//| Handle toggle theme button click                                 |
//+------------------------------------------------------------------+
void OnToggleThemeButtonClick()
{
    darkTheme = !darkTheme;
    UpdateThemeColors();
    Print("Theme toggled: ", darkTheme ? "Dark" : "Light");
}

//+------------------------------------------------------------------+
//| Update theme colors for the panel                                |
//+------------------------------------------------------------------+
void UpdateThemeColors()
{
    color textColor = darkTheme ? clrWhite : clrBlack;
    color buttonBgColor = darkTheme ? clrDarkSlateGray : clrGainsboro;
    color borderColor = darkTheme ? clrSlateGray : clrGray;
    color bgColor = darkTheme ? clrDarkBlue : clrWhite;

    
    UpdateButtonTheme(clearButton, textColor, buttonBgColor, borderColor);
    UpdateButtonTheme(sendButton, textColor, buttonBgColor, borderColor);
    UpdateButtonTheme(toggleThemeButton, textColor, buttonBgColor, borderColor);
    UpdateButtonTheme(changeFontButton, textColor, buttonBgColor, borderColor);
    UpdateButtonTheme(minimizeButton, textColor, buttonBgColor, borderColor);
    UpdateButtonTheme(maximizeButton, textColor, buttonBgColor, borderColor);
    UpdateButtonTheme(closeButton, textColor, buttonBgColor, borderColor);

    for (int i = 0; i < ArraySize(quickMessageButtons); i++)
    {
        UpdateButtonTheme(quickMessageButtons[i], textColor, buttonBgColor, borderColor);
    }

    ChartRedraw();
}

//+------------------------------------------------------------------+
//| Apply theme settings to a button                                 |
//+------------------------------------------------------------------+
void UpdateButtonTheme(CButton &button, color textColor, color bgColor, color borderColor)
{
    button.SetTextColor(textColor);
    button.SetBackgroundColor(bgColor);
    button.SetBorderColor(borderColor);
}

//+------------------------------------------------------------------+
//| Handle change font button click                                  |
//+------------------------------------------------------------------+
void OnChangeFontButtonClick()
{
    currentFontIndex = (currentFontIndex + 1) % ArraySize(availableFonts);
    SetFontForAll(availableFonts[currentFontIndex]);
    Print("Font changed to: ", availableFonts[currentFontIndex]);
    ChartRedraw();
}

//+------------------------------------------------------------------+
//| Set font for all input boxes and buttons                         |
//+------------------------------------------------------------------+
void SetFontForAll(string fontName)
{
    inputBox.Font(fontName);
    clearButton.Font(fontName);
    sendButton.Font(fontName);
    toggleThemeButton.Font(fontName);
    changeFontButton.Font(fontName);
    minimizeButton.Font(fontName);
    maximizeButton.Font(fontName);
    closeButton.Font(fontName);

    for (int i = 0; i < ArraySize(quickMessageButtons); i++)
    {
        quickMessageButtons[i].Font(fontName);
    }
}
//+------------------------------------------------------------------+
//| Generate a random 6-digit code for 2FA                           |
//+------------------------------------------------------------------+
string GenerateRandom6DigitCode()
{
    int code = MathRand() % 1000000; // Produces a 6-digit number
    return StringFormat("%06d", code); // Ensures leading zeros
}

//+------------------------------------------------------------------+
//| Handle minimize button click                                     |
//+------------------------------------------------------------------+
void OnMinimizeButtonClick()
{
    minimized = true;
    adminPanel.Hide();
    minimizeButton.Hide();
    maximizeButton.Show();
    closeButton.Show();
    Print("Panel minimized.");
}

//+------------------------------------------------------------------+
//| Handle maximize button click                                     |
//+------------------------------------------------------------------+
void OnMaximizeButtonClick()
{
    if (minimized)
    {
        adminPanel.Show();
        minimizeButton.Show();
        maximizeButton.Hide();
        closeButton.Hide();
        minimized = false;
        Print("Panel maximized.");
    }
}

//+------------------------------------------------------------------+
//| Handle close button click for admin panel                        |
//+------------------------------------------------------------------+
void OnCloseButtonClick()
{
    ExpertRemove();
    Print("Admin panel closed.");
}

//+------------------------------------------------------------------+
//| Send the message to Telegram                                     |
//+------------------------------------------------------------------+
bool SendMessageToTelegram(string message, string chatId, string botToken)
{
    string url = "https://api.telegram.org/bot" + botToken + "/sendMessage";
    string jsonMessage = "{\"chat_id\":\"" + chatId + "\", \"text\":\"" + message + "\"}";

    char postData[];
    ArrayResize(postData, StringToCharArray(jsonMessage, postData) - 1);

    int timeout = 5000;
    char result[];
    string responseHeaders;
    int responseCode = WebRequest("POST", url, "Content-Type: application/json\r\n", timeout, postData, result, responseHeaders);

    if (responseCode == 200)
    {
        Print("Message sent successfully: ", message);
        return true;
    }
    else
    {
        Print("Failed to send message. HTTP code: ", responseCode, " Error code: ", GetLastError());
        Print("Response: ", CharArrayToString(result));
        return false;
    }
}

//+------------------------------------------------------------------+


テストと結果

最後に、管理パネルに強力なパスワードセキュリティと2要素認証(2FA)を実装しました。以下に応答を示す画像を示します。

間違ったパスワードの試行

間違ったパスワードの試行

間違った認証コードの試行

認証コードの入力が間違っている

次の画像は、完全なログインおよび検証プロセスを示しています。また、Telegramアプリをログイン手順と同期させて、確認コードの配信をキャプチャすることにも成功しました。

完全なログインテストパスワードと2FA

完全なログインパスワードと2FAテスト

Telegramでの2FAコードの配信

上記のログイン試行に対するTelegramでの2FAコードの配信


結論

このMQL5プロジェクトに2要素認証(2FA)機能を組み込むことで、ユーザーアクセスの検証に重要なレイヤーが追加され、管理パネルのセキュリティが大幅に向上しました。コード配信にTelegramを活用したことで、ユーザーはリアルタイムで通知を受け取れるようになりました。実装には誤入力へのエラーハンドリングも含まれており、ユーザーがパスワードまたは2FAコードの入力を再試行できるよう促します。これにより、不正アクセスを最小限に抑えつつ、ユーザーに間違いを通知して必要なアクションを促す仕組みが実現されました。

ただし、特に注意すべき点として、Telegramアプリケーションが同じコンピューターにインストールされ、ログイン状態のままである場合、一定のリスクが残ることを認識する必要があります。デバイスが侵害されたり、システム管理者がTelegramに使用している電話に権限のないユーザーがアクセスしたりした場合、侵入者はそのような脆弱性を悪用する可能性があります。そのため、使用していない際はアプリからログアウトしておくことや、デバイスのセキュリティを確保することなど、厳格なセキュリティ対策を講じることが、機密データや通信を保護する上で非常に重要です。

特に、リアルタイムコード生成や安全なメッセージングにおける2FAのためのMQL5実装について貴重な知見を得られたことを願っています。これらの概念を理解することで、アプリケーションのセキュリティが一層強化され、潜在的な脅威に対する予防策の重要性を認識するきっかけとなるでしょう。以下に最終成果物を添付してあります。開発をお楽しみください。トレーダーの皆さん、頑張りましょう。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/16142

添付されたファイル |
Admin__Panel.mq5 (21.85 KB)
Controlsクラスを使用してインタラクティブなMQL5ダッシュボード/パネルを作成する方法(第2回):ボタンの応答性の追加 Controlsクラスを使用してインタラクティブなMQL5ダッシュボード/パネルを作成する方法(第2回):ボタンの応答性の追加
この記事では、ボタンの応答性を有効にすることで、静的なMQL5ダッシュボードパネルをインタラクティブなツールへと変換することに焦点を当てます。GUIコンポーネントの機能を自動化し、ユーザーのクリックに適切に反応する方法を探究します。この記事の最後には、ユーザーのエンゲージメントと取引体験を向上させる動的なインターフェイスを構築します。
Candlestick Trend Constraintモデルの構築(第9回):マルチ戦略エキスパートアドバイザー(II) Candlestick Trend Constraintモデルの構築(第9回):マルチ戦略エキスパートアドバイザー(II)
エキスパートアドバイザー(EA)に統合できる戦略の数は、事実上無限と言えます。しかし、戦略を追加するたびにアルゴリズムの複雑さが増していきます。複数の戦略を組み込むことで、EAは多様な市場環境により柔軟に適応し、収益性を向上させる可能性が高まります。本日は、Trend Constraint EAの機能をさらに強化するための取り組みとして、リチャード・ドンチャンが開発した著名な戦略のひとつを対象に、MQL5を活用する方法をご紹介します。
Connexusのリクエスト(第6回):HTTPリクエストとレスポンスの作成 Connexusのリクエスト(第6回):HTTPリクエストとレスポンスの作成
Connexusライブラリ連載第6回目では、HTTPリクエストの構成要素全体に焦点を当て、リクエストを構成する各コンポーネントを取り上げます。そして、リクエスト全体を表現するクラスを作成し、これまでに作成したクラスを統合します。
Connexusヘルパー(第5回):HTTPメソッドとステータスコード Connexusヘルパー(第5回):HTTPメソッドとステータスコード
この記事では、Web上でクライアントとサーバー間の重要な通信手段であるHTTPメソッドとステータスコードについて理解します。各メソッドの役割を理解することで、リクエストをより正確に制御できるようになり、サーバーに対して実行したいアクションを明確に伝えることができます。これにより、通信の効率が向上します。