1
0
mirror of https://git.teknik.io/Teknikode/Teknik.git synced 2023-08-02 14:16:22 +02:00

Added trusted device support for Two Factor authentication.

This commit is contained in:
Uncled1023 2016-06-30 17:00:44 -07:00
parent 011e737846
commit 000f977dfe
17 changed files with 202 additions and 44 deletions

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Teknik.Helpers;
namespace Teknik
{

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Teknik.Areas.Shortener.Models;
using Teknik.Helpers;
using Teknik.Models;
namespace Teknik.Areas.Shortener

View File

@ -5,6 +5,7 @@ using System.Web;
using System.IO;
using Teknik.Configuration;
using Teknik.Models;
using Teknik.Helpers;
namespace Teknik.Areas.Upload
{

View File

@ -23,6 +23,7 @@ using Teknik.Filters;
using QRCoder;
using System.Text;
using TwoStepsAuthenticator;
using System.Drawing;
namespace Teknik.Areas.Users.Controllers
{
@ -167,10 +168,30 @@ namespace Teknik.Areas.Users.Controllers
bool userValid = UserHelper.UserPasswordCorrect(db, Config, user, model.Password);
if (userValid)
{
bool twoFactor = false;
string returnUrl = model.ReturnUrl;
if (user.SecuritySettings.TwoFactorEnabled)
{
twoFactor = true;
// We need to check their device, and two factor them
if (user.SecuritySettings.AllowTrustedDevices)
{
// Check for the trusted device cookie
HttpCookie cookie = Request.Cookies[Constants.TRUSTEDDEVICECOOKIE + "_" + username];
if (cookie != null)
{
string token = cookie.Value;
if (user.TrustedDevices.Where(d => d.Token == token).FirstOrDefault() != null)
{
// The device token is attached to the user, let's let it slide
twoFactor = false;
}
}
}
}
if (twoFactor)
{
Session["AuthenticatedUser"] = user;
if (string.IsNullOrEmpty(model.ReturnUrl))
returnUrl = Request.UrlReferrer.AbsoluteUri.ToString();
@ -295,7 +316,7 @@ namespace Teknik.Areas.Users.Controllers
}
[HttpPost]
public ActionResult Edit(string curPass, string newPass, string newPassConfirm, string pgpPublicKey, string recoveryEmail, bool twoFactorEnabled, string website, string quote, string about, string blogTitle, string blogDesc, bool saveKey, bool serverSideEncrypt)
public ActionResult Edit(string curPass, string newPass, string newPassConfirm, string pgpPublicKey, string recoveryEmail, bool allowTrustedDevices, bool twoFactorEnabled, string website, string quote, string about, string blogTitle, string blogDesc, bool saveKey, bool serverSideEncrypt)
{
if (ModelState.IsValid)
{
@ -329,6 +350,7 @@ namespace Teknik.Areas.Users.Controllers
}
user.SecuritySettings.PGPSignature = pgpPublicKey;
// Recovery Email
bool newRecovery = false;
if (recoveryEmail != user.SecuritySettings.RecoveryEmail)
{
@ -337,24 +359,56 @@ namespace Teknik.Areas.Users.Controllers
user.SecuritySettings.RecoveryVerified = false;
}
// Trusted Devices
user.SecuritySettings.AllowTrustedDevices = allowTrustedDevices;
if (!allowTrustedDevices)
{
// They turned it off, let's clear the trusted devices
user.TrustedDevices.Clear();
List<TrustedDevice> foundDevices = db.TrustedDevices.Where(d => d.UserId == user.UserId).ToList();
if (foundDevices != null)
{
foreach (TrustedDevice device in foundDevices)
{
db.TrustedDevices.Remove(device);
}
}
}
// Two Factor Authentication
bool oldTwoFactor = user.SecuritySettings.TwoFactorEnabled;
user.SecuritySettings.TwoFactorEnabled = twoFactorEnabled;
string newKey = string.Empty;
if (twoFactorEnabled)
if (!oldTwoFactor && twoFactorEnabled)
{
// They just enabled it, let's regen the key
newKey = Authenticator.GenerateKey();
}
else if (!twoFactorEnabled)
{
// remove the key when it's disabled
newKey = string.Empty;
}
else
{
// No change, let's use the old value
newKey = user.SecuritySettings.TwoFactorKey;
}
user.SecuritySettings.TwoFactorKey = newKey;
// Profile Info
user.UserSettings.Website = website;
user.UserSettings.Quote = quote;
user.UserSettings.About = about;
// Blogs
user.BlogSettings.Title = blogTitle;
user.BlogSettings.Description = blogDesc;
// Uploads
user.UploadSettings.SaveKey = saveKey;
user.UploadSettings.ServerSideEncrypt = serverSideEncrypt;
UserHelper.EditAccount(db, Config, user, changePass, newPass);
// If they have a recovery email, let's send a verification
@ -557,18 +611,24 @@ namespace Teknik.Areas.Users.Controllers
[AllowAnonymous]
public ActionResult ConfirmTwoFactorAuth(string returnUrl, bool rememberMe)
{
ViewBag.Title = "Unknown Device - " + Config.Title;
ViewBag.Description = "We do not recognize this device.";
LoginViewModel model = new LoginViewModel();
model.ReturnUrl = returnUrl;
model.RememberMe = rememberMe;
User user = (User)Session["AuthenticatedUser"];
if (user != null)
{
ViewBag.Title = "Unknown Device - " + Config.Title;
ViewBag.Description = "We do not recognize this device.";
TwoFactorViewModel model = new TwoFactorViewModel();
model.ReturnUrl = returnUrl;
model.RememberMe = rememberMe;
model.AllowTrustedDevice = user.SecuritySettings.AllowTrustedDevices;
return View("/Areas/User/Views/User/TwoFactorCheck.cshtml", model);
return View("/Areas/User/Views/User/TwoFactorCheck.cshtml", model);
}
return Redirect(Url.SubRouteUrl("error", "Error.Http403"));
}
[HttpPost]
[AllowAnonymous]
public ActionResult ConfirmAuthenticatorCode(string code, string returnUrl, bool rememberMe)
public ActionResult ConfirmAuthenticatorCode(string code, string returnUrl, bool rememberMe, bool rememberDevice, string deviceName)
{
User user = (User)Session["AuthenticatedUser"];
if (user != null)
@ -586,6 +646,23 @@ namespace Teknik.Areas.Users.Controllers
HttpCookie authcookie = UserHelper.CreateAuthCookie(user.Username, rememberMe, Request.Url.Host.GetDomain(), Request.IsLocal);
Response.Cookies.Add(authcookie);
if (user.SecuritySettings.AllowTrustedDevices && rememberDevice)
{
// They want to remember the device, and have allow trusted devices on
HttpCookie trustedDeviceCookie = UserHelper.CreateTrustedDeviceCookie(user.Username, Request.Url.Host.GetDomain(), Request.IsLocal);
Response.Cookies.Add(trustedDeviceCookie);
TrustedDevice device = new TrustedDevice();
device.UserId = user.UserId;
device.Name = (string.IsNullOrEmpty(deviceName)) ? "Unknown" : deviceName;
device.DateSeen = DateTime.Now;
device.Token = trustedDeviceCookie.Value;
// Add the token
db.TrustedDevices.Add(device);
db.SaveChanges();
}
if (string.IsNullOrEmpty(returnUrl))
returnUrl = Request.UrlReferrer.AbsoluteUri.ToString();
return Json(new { result = returnUrl });
@ -628,9 +705,9 @@ namespace Teknik.Areas.Users.Controllers
QRCodeGenerator qrGenerator = new QRCodeGenerator();
QRCodeData qrCodeData = qrGenerator.CreateQrCode(ProvisionUrl, QRCodeGenerator.ECCLevel.Q);
SvgQRCode qrCode = new SvgQRCode(qrCodeData);
string qrCodeImage = qrCode.GetGraphic(20);
return File(Encoding.UTF8.GetBytes(qrCodeImage), "image/svg+xml");
QRCode qrCode = new QRCode(qrCodeData);
Bitmap qrCodeImage = qrCode.GetGraphic(20);
return File(Helpers.Utility.ImageToByte(qrCodeImage), "image/png");
}
}
}

View File

@ -24,6 +24,8 @@ namespace Teknik.Areas.Users.Models
public bool RecoveryVerified { get; set; }
public bool AllowTrustedDevices { get; set; }
public bool TwoFactorEnabled { get; set; }
[CaseSensitive]
@ -35,6 +37,7 @@ namespace Teknik.Areas.Users.Models
{
RecoveryEmail = string.Empty;
RecoveryVerified = false;
AllowTrustedDevices = false;
TwoFactorEnabled = false;
TwoFactorKey = string.Empty;
PGPSignature = string.Empty;

View File

@ -6,9 +6,9 @@ using Teknik.Attributes;
namespace Teknik.Areas.Users.Models
{
public class UserDevice
public class TrustedDevice
{
public int UserDeviceId { get; set; }
public int TrustedDeviceId { get; set; }
public int UserId { get; set; }

View File

@ -33,7 +33,7 @@ namespace Teknik.Areas.Users.Models
public virtual UploadSettings UploadSettings { get; set; }
public virtual ICollection<UserDevice> Devices { get; set; }
public virtual ICollection<TrustedDevice> TrustedDevices { get; set; }
public virtual ICollection<Upload.Models.Upload> Uploads { get; set; }
@ -47,7 +47,7 @@ namespace Teknik.Areas.Users.Models
JoinDate = DateTime.Now;
LastSeen = DateTime.Now;
Groups = new List<Group>();
Devices = new List<UserDevice>();
TrustedDevices = new List<TrustedDevice>();
}
}
}

View File

@ -2,16 +2,18 @@
$("#authCheckStatus").css('display', 'none', 'important');
$("#verifyCodeSubmit").click(function () {
setCode = $("#code").val();
returnUrl = $("#returnUrl").val();
rememberMe = ($("#rememberMe").val() == 'True');
setCode = $("#Code").val();
returnUrl = $("#ReturnUrl").val();
rememberMe = ($("#RememberMe").val() == 'True');
rememberDevice = $("#RememberDevice").is(":checked");
$.ajax({
type: "POST",
url: confirmAuthCodeURL,
data: {
code: setCode,
returnUrl: returnUrl,
rememberMe: rememberMe
rememberMe: rememberMe,
rememberDevice: rememberDevice
},
xhrFields: {
withCredentials: true

View File

@ -2,6 +2,7 @@
$("[name='update_upload_saveKey']").bootstrapSwitch();
$("[name='update_upload_serverSideEncrypt']").bootstrapSwitch();
$("[name='update_security_two_factor']").bootstrapSwitch();
$("[name='update_security_allow_trusted']").bootstrapSwitch();
$('#ResendVerification').click(function () {
$.ajax({
@ -98,6 +99,7 @@
password = $("#update_password").val();
password_confirm = $("#update_password_confirm").val();
update_pgp_public_key = $("#update_pgp_public_key").val();
update_security_allow_trusted = $("#update_security_allow_trusted").is(":checked");
update_security_two_factor = $("#update_security_two_factor").is(":checked");
recovery = $("#update_recovery_email").val();
website = $("#update_website").val();
@ -115,6 +117,7 @@
newPass: password,
newPassConfirm: password_confirm,
pgpPublicKey: update_pgp_public_key,
allowTrustedDevices: update_security_allow_trusted,
twoFactorEnabled: update_security_two_factor,
recoveryEmail: recovery,
website: website,

View File

@ -437,7 +437,7 @@ namespace Teknik.Areas.Users.Utility
}
// Create a new verification code and add it
string verifyCode = Teknik.Utility.RandomString(24);
string verifyCode = Helpers.Utility.RandomString(24);
RecoveryEmailVerification ver = new RecoveryEmailVerification();
ver.UserId = user.UserId;
ver.Code = verifyCode;
@ -518,7 +518,7 @@ Thank you and enjoy!
}
// Create a new verification code and add it
string verifyCode = Teknik.Utility.RandomString(24);
string verifyCode = Helpers.Utility.RandomString(24);
ResetPasswordVerification ver = new ResetPasswordVerification();
ver.UserId = user.UserId;
ver.Code = verifyCode;
@ -858,7 +858,7 @@ If you recieved this email and you did not reset your password, you can ignore t
{
Config config = Config.Load();
HttpCookie authcookie = FormsAuthentication.GetAuthCookie(username, remember);
authcookie.Name = "TeknikAuth";
authcookie.Name = Constants.AUTHCOOKIE;
authcookie.HttpOnly = true;
authcookie.Secure = true;
@ -878,5 +878,35 @@ If you recieved this email and you did not reset your password, you can ignore t
return authcookie;
}
public static HttpCookie CreateTrustedDeviceCookie(string username, string domain, bool local)
{
Config config = Config.Load();
byte[] time = BitConverter.GetBytes(DateTime.UtcNow.ToBinary());
byte[] key = Guid.NewGuid().ToByteArray();
string token = Convert.ToBase64String(time.Concat(key).ToArray());
HttpCookie trustCookie = new HttpCookie(Constants.TRUSTEDDEVICECOOKIE + "_" + username);
trustCookie.Value = token;
trustCookie.HttpOnly = true;
trustCookie.Secure = true;
trustCookie.Expires = DateTime.Now.AddYears(1);
// Set domain dependent on where it's being ran from
if (local) // localhost
{
trustCookie.Domain = null;
}
else if (config.DevEnvironment) // dev.example.com
{
trustCookie.Domain = string.Format("dev.{0}", domain);
}
else // A production instance
{
trustCookie.Domain = string.Format(".{0}", domain);
}
return trustCookie;
}
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Teknik.ViewModels;
namespace Teknik.Areas.Users.ViewModels
{
public class TwoFactorViewModel : ViewModelBase
{
public bool RememberMe { get; set; }
public string ReturnUrl { get; set; }
public bool AllowTrustedDevice { get; set; }
}
}

View File

@ -125,19 +125,6 @@
<textarea class="form-control" id="update_pgp_public_key" name="update_pgp_public_key" placeholder="Public Key Here" title="enter your pgp public key" rows="15">@Model.SecuritySettings.PGPSignature</textarea>
</div>
</div>
<div class="row">
<div class="col-sm-6 text-left">
<label for="update_security_two_factor"><h4>Enable Two Factor Authentication</h4></label>
<div class="checkbox">
<label>
<input id="update_security_two_factor" name="update_security_two_factor" title="whether the key should be saved on the server or not" type="checkbox" value="true" @(Model.SecuritySettings.TwoFactorEnabled ? "checked" : string.Empty) />
</label>
</div>
<p class="form-control-static @(Model.SecuritySettings.TwoFactorEnabled ? string.Empty : "hide")" id="setupAuthenticatorLink">
<small><a href="#" class="text-primary" id="SetupAuthenticator" data-toggle="modal" data-target="#authenticatorSetup"><i class="fa fa-lock"></i> Set Up Authenticator</a></small>
</p>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<div class="row">
@ -161,6 +148,27 @@
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4 text-left">
<label for="update_security_two_factor"><h4>Enable Two Factor Authentication</h4></label>
<div class="checkbox">
<label>
<input id="update_security_two_factor" name="update_security_two_factor" title="whether two factor authentication should occur for this account" type="checkbox" value="true" @(Model.SecuritySettings.TwoFactorEnabled ? "checked" : string.Empty) />
</label>
</div>
<p class="form-control-static @(Model.SecuritySettings.TwoFactorEnabled ? string.Empty : "hide")" id="setupAuthenticatorLink">
<small><a href="#" class="text-primary" id="SetupAuthenticator" data-toggle="modal" data-target="#authenticatorSetup"><i class="fa fa-lock"></i> Set Up Authenticator</a></small>
</p>
</div>
<div class="col-sm-4 text-left">
<label for="update_security_allow_trusted"><h4>Allow Trusted Device Saving</h4></label>
<div class="checkbox">
<label>
<input id="update_security_allow_trusted" name="update_security_allow_trusted" title="whether a device could be cached to bypass two factor authentication" type="checkbox" value="true" @(Model.SecuritySettings.AllowTrustedDevices ? "checked" : string.Empty) />
</label>
</div>
</div>
</div>
</div>
<!-- Blog Settings -->
<div class="tab-pane" id="blog">

View File

@ -1,4 +1,4 @@
@model Teknik.Areas.Users.ViewModels.LoginViewModel
@model Teknik.Areas.Users.ViewModels.TwoFactorViewModel
@using Teknik.Helpers
@ -22,12 +22,23 @@
</div>
</div>
<form role="form" id="twoFactorCheckForm" action="##" method="post" accept-charset="UTF-8">
<input name="returnUrl" id="returnUrl" type="hidden" value="@Model.ReturnUrl" />
<input name="rememberMe" id="rememberMe" type="hidden" value="@Model.RememberMe" />
<input name="ReturnUrl" id="ReturnUrl" type="hidden" value="@Model.ReturnUrl" />
<input name="RememberMe" id="RememberMe" type="hidden" value="@Model.RememberMe" />
<div class="form-group text-left">
<label for="update_website">Authentication code</label>
<input type="text" class="form-control" id="code" name="code" data-val-required="The Authentication Code is required." data-val="true" />
<input type="text" class="form-control" id="Code" name="Code" data-val-required="The Authentication Code is required." data-val="true" />
</div>
@if (Model.AllowTrustedDevice)
{
<div class="checkbox">
<label>
<input id="RememberDevice" type="checkbox" value="true" name="RememberDevice" /><input name="RememberDevice" type="hidden" value="false"> Remember Device
</label>
</div>
<small>Set this device as a trusted device. It is not advised to trust a public computer.</small>
<br />
<br />
}
<div class="form-group">
<button class="btn btn-primary btn-block" id="verifyCodeSubmit" type="button" name="verifyCodeSubmit">Verify</button>
</div>

View File

@ -9,6 +9,9 @@ namespace Teknik.Helpers
public static class Constants
{
public const string SERVERUSER = "Server Admin";
public const string AUTHCOOKIE = "TeknikAuth";
public const string TRUSTEDDEVICECOOKIE = "TeknikTrustedDevice";
// Paste Constants
public static Dictionary<string, string> HIGHLIGHTFORMATS = new Dictionary<string, string>()
{

View File

@ -8,7 +8,7 @@ using System.Linq;
using System.Text;
using System.Web;
namespace Teknik
namespace Teknik.Helpers
{
public static class Utility
{

View File

@ -22,7 +22,7 @@ namespace Teknik.Models
public DbSet<User> Users { get; set; }
public DbSet<Group> Groups { get; set; }
public DbSet<Role> Roles { get; set; }
public DbSet<UserDevice> UserDevices { get; set; }
public DbSet<TrustedDevice> TrustedDevices { get; set; }
public DbSet<TransferType> TransferTypes { get; set; }
// User Settings
public DbSet<UserSettings> UserSettings { get; set; }
@ -112,7 +112,7 @@ namespace Teknik.Models
modelBuilder.Entity<User>().ToTable("Users");
modelBuilder.Entity<Group>().ToTable("Groups");
modelBuilder.Entity<Role>().ToTable("Roles");
modelBuilder.Entity<UserDevice>().ToTable("UserDevices");
modelBuilder.Entity<TrustedDevice>().ToTable("TrustedDevices");
modelBuilder.Entity<TransferType>().ToTable("TransferTypes");
modelBuilder.Entity<RecoveryEmailVerification>().ToTable("RecoveryEmailVerifications");
modelBuilder.Entity<ResetPasswordVerification>().ToTable("ResetPasswordVerifications");

View File

@ -224,7 +224,8 @@
<Compile Include="Areas\User\Models\ResetPasswordVerification.cs" />
<Compile Include="Areas\User\Models\RecoveryEmailVerification.cs" />
<Compile Include="Areas\User\Models\SecuritySettings.cs" />
<Compile Include="Areas\User\Models\UserDevice.cs" />
<Compile Include="Areas\User\Models\TrustedDevice.cs" />
<Compile Include="Areas\User\ViewModels\TwoFactorViewModel.cs" />
<Compile Include="Models\TransferTypes.cs" />
<Compile Include="Areas\User\Models\UploadSettings.cs" />
<Compile Include="Areas\User\Models\UserSettings.cs" />