diff --git a/src/Core/Utilities/DuoWeb.cs b/src/Core/Utilities/DuoWeb.cs
new file mode 100644
index 0000000000..e374e76490
--- /dev/null
+++ b/src/Core/Utilities/DuoWeb.cs
@@ -0,0 +1,242 @@
+/*
+Original source modified from https://github.com/duosecurity/duo_dotnet
+
+=============================================================================
+=============================================================================
+
+ref: https://github.com/duosecurity/duo_dotnet/blob/master/LICENSE
+
+Copyright (c) 2011, Duo Security, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+3. The name of the author may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+using System;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Bit.Core.Utilities.Duo
+{
+ public static class DuoWeb
+ {
+ private const string DuoProfix = "TX";
+ private const string AppPrefix = "APP";
+ private const string AuthPrefix = "AUTH";
+ private const int DuoExpire = 300;
+ private const int AppExpire = 3600;
+ private const int IKeyLength = 20;
+ private const int SKeyLength = 40;
+ private const int AKeyLength = 40;
+
+ public static string ErrorUser = "ERR|The username passed to sign_request() is invalid.";
+ public static string ErrorIKey = "ERR|The Duo integration key passed to sign_request() is invalid.";
+ public static string ErrorSKey = "ERR|The Duo secret key passed to sign_request() is invalid.";
+ public static string ErrorAKey = "ERR|The application secret key passed to sign_request() must be at least " +
+ "40 characters.";
+ public static string ErrorUnknown = "ERR|An unknown error has occurred.";
+
+ // throw on invalid bytes
+ private static Encoding _encoding = new UTF8Encoding(false, true);
+ private static DateTime _epoc = new DateTime(1970, 1, 1);
+
+ ///
+ /// Generate a signed request for Duo authentication.
+ /// The returned value should be passed into the Duo.init() call
+ /// in the rendered web page used for Duo authentication.
+ ///
+ /// Duo integration key
+ /// Duo secret key
+ /// Application secret key
+ /// Primary-authenticated username
+ /// (optional) The current UTC time
+ /// signed request
+ public static string SignRequest(string ikey, string skey, string akey, string username,
+ DateTime? currentTime = null)
+ {
+ string duoSig;
+ string appSig;
+
+ var currentTimeValue = currentTime ?? DateTime.UtcNow;
+
+ if(username == string.Empty)
+ {
+ return ErrorUser;
+ }
+ if(username.Contains("|"))
+ {
+ return ErrorUser;
+ }
+ if(ikey.Length != IKeyLength)
+ {
+ return ErrorIKey;
+ }
+ if(skey.Length != SKeyLength)
+ {
+ return ErrorSKey;
+ }
+ if(akey.Length < AKeyLength)
+ {
+ return ErrorAKey;
+ }
+
+ try
+ {
+ duoSig = SignVals(skey, username, ikey, DuoProfix, DuoExpire, currentTimeValue);
+ appSig = SignVals(akey, username, ikey, AppPrefix, AppExpire, currentTimeValue);
+ }
+ catch
+ {
+ return ErrorUnknown;
+ }
+
+ return $"{duoSig}:{appSig}";
+ }
+
+ ///
+ /// Validate the signed response returned from Duo.
+ /// Returns the username of the authenticated user, or null.
+ ///
+ /// Duo integration key
+ /// Duo secret key
+ /// Application secret key
+ /// The signed response POST'ed to the server
+ /// (optional) The current UTC time
+ /// authenticated username, or null
+ public static string VerifyResponse(string ikey, string skey, string akey, string sigResponse,
+ DateTime? currentTime = null)
+ {
+ string authUser = null;
+ string appUser = null;
+ var currentTimeValue = currentTime ?? DateTime.UtcNow;
+
+ try
+ {
+ var sigs = sigResponse.Split(':');
+ var authSig = sigs[0];
+ var appSig = sigs[1];
+
+ authUser = ParseVals(skey, authSig, AuthPrefix, ikey, currentTimeValue);
+ appUser = ParseVals(akey, appSig, AppPrefix, ikey, currentTimeValue);
+ }
+ catch
+ {
+ return null;
+ }
+
+ if(authUser != appUser)
+ {
+ return null;
+ }
+
+ return authUser;
+ }
+
+ private static string SignVals(string key, string username, string ikey, string prefix, long expire,
+ DateTime currentTime)
+ {
+ var ts = (long)(currentTime - _epoc).TotalSeconds;
+ expire = ts + expire;
+ var val = $"{username}|{ikey}|{expire.ToString()}";
+ var cookie = $"{prefix}|{Encode64(val)}";
+ var sig = Sign(key, cookie);
+ return $"{cookie}|{sig}";
+ }
+
+ private static string ParseVals(string key, string val, string prefix, string ikey, DateTime currentTime)
+ {
+ var ts = (long)(currentTime - _epoc).TotalSeconds;
+
+ var parts = val.Split('|');
+ if(parts.Length != 3)
+ {
+ return null;
+ }
+
+ var uPrefix = parts[0];
+ var uB64 = parts[1];
+ var uSig = parts[2];
+
+ var sig = Sign(key, $"{uPrefix}|{uB64}");
+ if(Sign(key, sig) != Sign(key, uSig))
+ {
+ return null;
+ }
+
+ if(uPrefix != prefix)
+ {
+ return null;
+ }
+
+ var cookie = Decode64(uB64);
+ var cookieParts = cookie.Split('|');
+ if(cookieParts.Length != 3)
+ {
+ return null;
+ }
+
+ var username = cookieParts[0];
+ var uIKey = cookieParts[1];
+ var expire = cookieParts[2];
+
+ if(uIKey != ikey)
+ {
+ return null;
+ }
+
+ var expireTs = Convert.ToInt32(expire);
+ if(ts >= expireTs)
+ {
+ return null;
+ }
+
+ return username;
+ }
+
+ private static string Sign(string skey, string data)
+ {
+ var keyBytes = Encoding.ASCII.GetBytes(skey);
+ var dataBytes = Encoding.ASCII.GetBytes(data);
+
+ using(var hmac = new HMACSHA1(keyBytes))
+ {
+ var hash = hmac.ComputeHash(dataBytes);
+ var hex = BitConverter.ToString(hash);
+ return hex.Replace("-", "").ToLower();
+ }
+ }
+
+ private static string Encode64(string plaintext)
+ {
+ var plaintextBytes = _encoding.GetBytes(plaintext);
+ return Convert.ToBase64String(plaintextBytes);
+ }
+
+ private static string Decode64(string encoded)
+ {
+ var plaintextBytes = Convert.FromBase64String(encoded);
+ return _encoding.GetString(plaintextBytes);
+ }
+ }
+}