iT邦幫忙

0

[C#] 如何使用 MOTP 搭配 OTP Authenticator App 產生一次性密碼登入(附範例下載)

在傳統的登入系統中總是使用帳號密碼的方式驗證身份,這種方式如果密碼不小心被盜取的話,帳號資料就會有被駭入的可能性。
為了提升帳號安全性,我們可以利用手機 App 產生一次性密碼 (MOTP),做為登入的第二道密碼使用又稱雙重驗證,這樣的優點是不容易受到攻擊,需要登入密碼及一次性密碼才可以完成登入。

這次的教學重點會放在如何與 OTP Authenticator 免費 App 搭配產生一次性密碼,並在網頁上驗證一次性密碼 (OTP)。

我寫了簡單的範例,在 C# Asp.Net 網頁產生註冊 QR Code,並利用免費的 OTP Authenticator App 掃描 QR Code 產生一次性密碼 (OTP) 後,再回到網頁上驗證身份。

範例建置環境
前端架構: Vue.js, jQuery, Bootstrap
後端架構: C# ASP.Net MVC .Net Framework

此登入範例為求重點展示 MOTP 所以沒有使用資料庫,大家了解 MOTP 規則後,可以應用在實務專案上。
文末會提供範例檔下載連結。

行動一次性密碼 (MOTP) 是什麼

行動一次性密碼(英語:Mobile One-Time Password,簡稱MOTP),又稱動態密碼或單次有效密碼,利用行動裝置上產生有效期只有一次的密碼。
有效期採計時制,通常可設定為 30 秒到兩分鐘不等,時間過了之後就失效,下次需要時再產生新密碼。

MOTP 產生密碼的方法是手機取得註冊時的密鑰 (Token) 與 Pin 之後,以當下時間利用 MD5 加密後產生密碼,並取前 6 碼為一次性密碼。

有關 MOTP 的原文介紹可參考: Mobile One Time Passwords

C# 產生使用者註冊 QR Code

在範例頁面中輸入 UserID, Pin 及 密鑰,就可以產生 OTP Authenticator 可註冊的 QR Code。

Pin 要求為 4 碼數字。密鑰要求為 16 或 32 碼字元,範例中會隨機產生 16 碼亂數。

接下來看一下程式碼部份

HTML

<div class="panel panel-default">
	<div class="panel-heading">建立使用者</div>
	<div class="panel-body">
		<div class="row">
			<div class="col-md-4">
				<div class="form-group">
					<label>登入 ID:</label>
					<input type="text" class="form-control" v-model="form.UserID">
				</div>
			</div>
			<div class="col-md-4">
				<div class="form-group">
					<label>PIN (4 個數字):</label>
					<input type="text" class="form-control" v-model="form.UserPin">
				</div>
			</div>
			<div class="col-md-4">
				<label>密鑰 (16 個字元):</label>
				<div class="input-group">
					<input type="text" class="form-control" v-model="form.UserKey">
					<div class="input-group-btn">
						<button class="btn btn-default" type="button" v-on:click="ChgKey()">
							更換
						</button>
					</div>
				</div>
			</div>
		</div>
		<button type="button" class="btn btn-primary" v-on:click="GenUserQRCode()">產生使用者 QR Code</button>
		<br />
		<img class="img-thumbnail" style="width: 300px;height:300px;" v-bind:src="form.QrCodePath">
	</div>
</div>

Javascript

// 產生使用者 QR Code
, GenUserQRCode: function () {
	var self = this;
	var postData = {};
	postData['UserID'] = self.form.UserID;
	postData['UserPin'] = self.form.UserPin;
	postData['UserKey'] = self.form.UserKey;
	$.blockUI({ message: '處理中...' });
	$.ajax({
		url:'@Url.Content("~/Home/GenUserQRCode")',
		method:'POST',
		dataType:'json',
		data: { inModel: postData, __RequestVerificationToken: self.GetToken() },
		success: function (datas) {
			if (datas.ErrMsg != '') {
				alert(datas.ErrMsg);
				$.unblockUI();
				return;
			}
			self.form.QrCodePath = datas.FileWebPath;
			$.unblockUI();
		},
		error: function (err) {
			alert(err.responseText);
			$.unblockUI();
		},
	});
}
// 更換密鑰
, ChgKey: function () {
	var self = this;
	var key = self.MarkRan(16);
	self.form.UserKey = key;
}
// 隨機密鑰
, MarkRan: function (length) {
	var result = '';
	var characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
	var charactersLength = characters.length;
	for (var i = 0; i < length; i++) {
		result += characters.charAt(Math.floor(Math.random() * charactersLength));
	}
	return result;
}

C# Controller

/// <summary>
/// 產生使用者 QR Code
/// </summary>
/// <param name="inModel"></param>
/// <returns></returns>
[ValidateAntiForgeryToken]
public ActionResult GenUserQRCode(GenUserQRCodeIn inModel)
{
	GenUserQRCodeOut outModel = new GenUserQRCodeOut();
	outModel.ErrMsg = "";
	if (inModel.UserKey.Length != 16)
	{
		outModel.ErrMsg = "密鑰長度需為 16 碼";
	}
	if (inModel.UserPin.Length != 4)
	{
		outModel.ErrMsg = "PIN 長度需為 4 碼";
	}
	int t = 0;
	if (int.TryParse(inModel.UserPin, out t) == false)
	{
		outModel.ErrMsg = "PIN 需為數字";
	}

	if (outModel.ErrMsg == "")
	{
		// 產生註冊資料 For OTP Authenticator
		string motpUser = "<?xml version=\"1.0\" encoding=\"utf-8\"?><SSLOTPAuthenticator><mOTPProfile><ProfileName>{0}</ProfileName><PINType>0</PINType><PINSecurity>0</PINSecurity><Secret>{1}</Secret><AlgorithmMode>0</AlgorithmMode></mOTPProfile></SSLOTPAuthenticator>";
		motpUser = string.Format(motpUser, inModel.UserID, inModel.UserKey);

		// QR Code 設定
		BarcodeWriter bw = new BarcodeWriter
		{
			Format = BarcodeFormat.QR_CODE,
			Options = new QrCodeEncodingOptions //設定大小
			{
				Height = 300,
				Width = 300,
			}
		};
		//產生QRcode
		var img = bw.Write(motpUser); //來源網址
		string FileName = "qrcode.png"; //產生圖檔名稱
		Bitmap myBitmap = new Bitmap(img);
		string FileWebPath = Server.MapPath("~/") + FileName; //完整路徑
		myBitmap.Save(FileWebPath, ImageFormat.Png);
		string FileWebUrl = Url.Content("~/") + FileName; // 產生網頁可看到的路徑
		outModel.FileWebPath = FileWebUrl;
	}

	// 輸出json
	ContentResult resultJson = new ContentResult();
	resultJson.ContentType = "application/json";
	resultJson.Content = JsonConvert.SerializeObject(outModel); ;
	return resultJson;
}

程式碼使用到 QR Code 元件,使用 NuGet 安裝 ZXing.Net 元件,安裝方法可參考: [C#]QR Code 網址產生與解析

此段程式碼要先產生可讀取的 XML 檔,例如

再將此 XML 轉為 QR Code 即可。

C# Model

public class GenUserQRCodeIn
{
	public string UserID { get; set; }
	public string UserPin { get; set; }
	public string UserKey { get; set; }
}

public class GenUserQRCodeOut
{
	public string ErrMsg { get; set; }
	public string FileWebPath { get; set; }
}

程式產生 QR Code 之後,接下來就要利用 OTP Authenticator App 來操作了。

手機下載安裝 OTP Authenticator

App 名稱: OTP Authenticator
官網連結: https://www.swiss-safelab.com/en-us/products/otpauthenticator.aspx
App 性質: 免費軟體
iOS App Store 下載: https://apps.apple.com/tw/app/otp-authenticator/id915359210
Android APK 下載: https://www.swiss-safelab.com/en-us/community/downloadcenter.aspx?Command=Core_Download&EntryId=1105

OTP Authenticator 是針對 Mobile-OTP,行動裝置雙因素身份驗證規則而開發的免費 App,由 Swiss SafeLab 所開發。
Android 版本在 Google Play 沒有連結,若下載連結失效,可至官網重新下載 Apk

OTP Authenticator 註冊使用者帳號

打開 OTP Authenticator 後,左側選單點擊「Profiles」

下方點擊「Create Profile」

點擊「Scan Profile」

掃描網頁上提供的 QR Code
完成後即會增加使用者列表

點擊名稱後,輸入註冊時的 Pin 4位數字,例如範例上的 「0000」

App 即會產生一次性密碼,每 30 秒會更換新密碼。此新密碼在網頁上使用者登入時會用到。

網頁使用者登入驗證 MOTP

在畫面上輸入登入ID, MOTP (手機上的一次性密碼),再驗證登入是否成功。

接下來看一下程式碼部份

HTML

<div class="panel panel-default">
	<div class="panel-heading">驗證登入</div>
	<div class="panel-body">
		<div class="row">
			<div class="col-md-4">
				<div class="form-group">
					<label>登入 ID:</label>
					<input type="text" class="form-control" v-model="form.UserID">
				</div>
			</div>
			<div class="col-md-4">
				<div class="form-group">
					<label>MOTP (6 個字元):</label>
					<input type="text" class="form-control" v-model="form.MOTP">
				</div>
			</div>

		</div>
		<button type="button" class="btn btn-primary" v-on:click="CheckLogin()">驗證登入</button>
		<br /><br />
		<span style="color:red;">檢核結果:{{form.CheckResult}}</span>

	</div>
</div>

Javascript

// 驗證登入
, CheckLogin: function () {
	var self = this;
	var postData = {};
	postData['UserID'] = self.form.UserID;
	postData['UserPin'] = self.form.UserPin;
	postData['UserKey'] = self.form.UserKey;
	postData['MOTP'] = self.form.MOTP;
	$.blockUI({ message: '處理中...' });
	$.ajax({
		url:'@Url.Content("~/Home/CheckLogin")',
		method:'POST',
		dataType:'json',
		data: { inModel: postData, __RequestVerificationToken: self.GetToken() },
		success: function (datas) {
			if (datas.ErrMsg != '') {
				alert(datas.ErrMsg);
				$.unblockUI();
				return;
			}
			self.form.CheckResult = datas.CheckResult;
			$.unblockUI();
		},
		error: function (err) {
			alert(err.responseText);
			$.unblockUI();
		},
	});
}

C# Controller

public decimal timeStampEpoch = (decimal)Math.Round((DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds, 0); //Unix timestamp

/// <summary>
/// 驗證登入
/// </summary>
/// <param name="inModel"></param>
/// <returns></returns>
[ValidateAntiForgeryToken]
public ActionResult CheckLogin(CheckLoginIn inModel)
{
	CheckLoginOut outModel = new CheckLoginOut();
	outModel.ErrMsg = "";
	if (inModel.MOTP == null || inModel.MOTP.Length != 6)
	{
		outModel.ErrMsg = "MOTP 長度需為 6 碼";
	}
	if (inModel.UserKey.Length != 16)
	{
		outModel.ErrMsg = "密鑰長度需為 16 碼";
	}
	if (inModel.UserPin.Length != 4)
	{
		outModel.ErrMsg = "PIN 長度需為 4 碼";
	}
	int t = 0;
	if (int.TryParse(inModel.UserPin, out t) == false)
	{
		outModel.ErrMsg = "PIN 需為數字";
	}

	if (outModel.ErrMsg == "")
	{
		outModel.CheckResult = "登入失敗";

		String otpCheckValueMD5 = "";
		decimal timeWindowInSeconds = 60; //1 分鐘前的 motp 都檢查
		for (decimal i = timeStampEpoch - timeWindowInSeconds; i <= timeStampEpoch + timeWindowInSeconds; i++)
		{
			otpCheckValueMD5 = (Md5Hash(((i.ToString()).Substring(0, (i.ToString()).Length - 1) + inModel.UserKey + inModel.UserPin))).Substring(0, 6);
			if (inModel.MOTP.ToLower() == otpCheckValueMD5.ToLower())
			{
				outModel.CheckResult = "登入成功";
				break;
			}
		}
	}

	// 輸出json
	ContentResult resultJson = new ContentResult();
	resultJson.ContentType = "application/json";
	resultJson.Content = JsonConvert.SerializeObject(outModel); ;
	return resultJson;
}

/// <summary>
/// MD5 編碼
/// </summary>
/// <param name="inputString"></param>
/// <returns></returns>
public string Md5Hash(string inputString)
{
	using (MD5 md5 = MD5.Create())
	{
		byte[] input = Encoding.UTF8.GetBytes(inputString);
		byte[] hash = md5.ComputeHash(input);
		string md5Str = BitConverter.ToString(hash).Replace("-", "");
		return md5Str;
	}
}

在驗證 OTP 時需要確認手機的時區和伺服器時區是一樣的,這樣才能檢查過去時間內有效的 OTP。
檢核使用 timestamp 加上密鑰及 Pin 用 MD5 加密,再取前 6 碼做為一次性密碼。
範例中以輸入的 OTP 與過去 1 分鐘內其中一組密碼相等即為登入成功。

C# Model

public class CheckLoginIn
{
	public string UserID { get; set; }
	public string UserPin { get; set; }
	public string UserKey { get; set; }
	public string MOTP { get; set; }
}

public class CheckLoginOut
{
	public string ErrMsg { get; set; }
	public string CheckResult { get; set; }
}

重點整理

  1. 產生使用者註冊 QR Code
  2. 安裝 OTP Authenticator
  3. OTP 掃描 QR Code
  4. 網頁登入驗證身份

範例下載

想要完整執行此範例可點擊下載: 範例下載

相關學習文章

如何避免 MS-SQL 暴力登入攻擊 (嘗試評估密碼時發生錯誤、找不到符合所提供名稱的登入)
[C#]QR Code 製作與 Base 64 編碼應用 (附範例)


尚未有邦友留言

立即登入留言