Browse Source

Changed auth backend to use Identity Server, and asp.net identities.

tags/3.0.0^2
Teknikode 1 year ago
parent
commit
7ec30d1e39

+ 7
- 0
Teknik/Areas/API/Controllers/APIv1Controller.cs View File

@@ -30,6 +30,7 @@ using Teknik.Logging;

namespace Teknik.Areas.API.Controllers
{
[Authorize]
[TeknikAuthorize(AuthType.Basic)]
[Area("APIv1")]
public class APIv1Controller : DefaultController
@@ -42,6 +43,12 @@ namespace Teknik.Areas.API.Controllers
return Redirect(Url.SubRouteUrl("help", "Help.API"));
}

[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> UploadAsync(APIv1UploadModel model)

+ 285
- 0
Teknik/Areas/Accounts/Controllers/AccountsController.cs View File

@@ -0,0 +1,285 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityServer4.Events;
using IdentityServer4.Extensions;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Teknik.Areas.Accounts.ViewModels;
using Teknik.Areas.Users.Models;
using Teknik.Areas.Users.Utility;
using Teknik.Configuration;
using Teknik.Controllers;
using Teknik.Data;
using Teknik.Logging;
using Teknik.Security;
using Teknik.Utilities;
using TwoStepsAuthenticator;

namespace Teknik.Areas.Accounts.Controllers
{
[Area("Accounts")]
public class AccountsController : DefaultController
{
private readonly UserStore _users;
private readonly SignInManager<User> _signInManager;
private readonly UserManager<User> _userManager;
private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clientStore;
private readonly IEventService _events;

private static readonly UsedCodesManager _usedCodesManager = new UsedCodesManager();

private const string _AuthSessionKey = "AuthenticatedUser";

public AccountsController(
ILogger<Logger> logger,
Config config,
TeknikEntities dbContext,
SignInManager<User> signInManager,
UserManager<User> userManager,
IIdentityServerInteractionService interaction,
IClientStore clientStore,
IEventService events,
UserStore users = null)
: base(logger, config, dbContext)
{
_users = users ?? new UserStore(_dbContext, _config);

_signInManager = signInManager;
_userManager = userManager;
_interaction = interaction;
_clientStore = clientStore;
_events = events;
}


[HttpGet]
[AllowAnonymous]
public IActionResult Login(string ReturnUrl)
{
LoginViewModel model = new LoginViewModel();
model.ReturnUrl = ReturnUrl;

return View("/Areas/Accounts/Views/Accounts/ViewLogin.cshtml", model);
}

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login([Bind(Prefix = "Login")]LoginViewModel model)
{
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);

if (ModelState.IsValid)
{
string username = model.Username;
User user = UserHelper.GetUser(_dbContext, username);
if (user != null)
{
// Make sure they aren't banned or anything
if (user.AccountStatus == AccountStatus.Banned)
{
model.Error = true;
model.ErrorMessage = "Account has been banned.";

// Raise the error event
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, model.ErrorMessage));

return GenerateActionResult(new { error = model.ErrorMessage }, View("/Areas/Accounts/Views/Accounts/ViewLogin.cshtml", model));
}

// Try to sign them in
var valid = await _userManager.CheckPasswordAsync(user, model.Password);
if (valid)
{
// Perform transfer actions on the account
UserHelper.TransferUser(_dbContext, _config, user, model.Password);
user.LastSeen = DateTime.Now;
_dbContext.Entry(user).State = EntityState.Modified;
_dbContext.SaveChanges();

// Let's double check their email and git accounts to make sure they exist
string email = UserHelper.GetUserEmailAddress(_config, username);
if (_config.EmailConfig.Enabled && !UserHelper.UserEmailExists(_config, email))
{
UserHelper.AddUserEmail(_config, email, model.Password);
}

if (_config.GitConfig.Enabled && !UserHelper.UserGitExists(_config, username))
{
UserHelper.AddUserGit(_config, username, model.Password);
}

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
string token = Request.Cookies[Constants.TRUSTEDDEVICECOOKIE + "_" + username];
if (!string.IsNullOrEmpty(token))
{
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)
{
HttpContext.Session.Set(_AuthSessionKey, user.Username);
if (string.IsNullOrEmpty(model.ReturnUrl))
returnUrl = Request.Headers["Referer"].ToString();
returnUrl = Url.SubRouteUrl("accounts", "Accounts.CheckAuthenticatorCode", new { returnUrl = returnUrl, rememberMe = model.RememberMe });
model.ReturnUrl = string.Empty;
}
else
{
returnUrl = Request.Headers["Referer"].ToString();

// They don't need two factor auth.
await SignInUser(user, (string.IsNullOrEmpty(model.ReturnUrl)) ? returnUrl : model.ReturnUrl, model.RememberMe);
}

if (string.IsNullOrEmpty(model.ReturnUrl))
{
return GenerateActionResult(new { result = returnUrl }, Redirect(returnUrl));
}
else
{
return Redirect(model.ReturnUrl);
}
}
}
}
// Raise the error event
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials"));

model.Error = true;
model.ErrorMessage = "Invalid Username or Password.";

return GenerateActionResult(new { error = model.ErrorMessage }, View("/Areas/Accounts/Views/Accounts/ViewLogin.cshtml", model));
}

/// <summary>
/// Handle logout page postback
/// </summary>
public async Task<IActionResult> Logout()
{
await LogoutUser(User, HttpContext, _signInManager, _events);

return Redirect(Url.SubRouteUrl("www", "Home.Index"));
}

[HttpGet]
[AllowAnonymous]
public IActionResult ConfirmTwoFactorAuth(string returnUrl, bool rememberMe)
{
string username = HttpContext.Session.Get<string>(_AuthSessionKey);
if (!string.IsNullOrEmpty(username))
{
User user = UserHelper.GetUser(_dbContext, username);
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/Accounts/Views/Accounts/TwoFactorCheck.cshtml", model);
}
return Redirect(Url.SubRouteUrl("error", "Error.Http403"));
}

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> ConfirmAuthenticatorCode(string code, string returnUrl, bool rememberMe, bool rememberDevice, string deviceName)
{
string errorMessage = string.Empty;
string username = HttpContext.Session.Get<string>(_AuthSessionKey);
if (!string.IsNullOrEmpty(username))
{
User user = UserHelper.GetUser(_dbContext, username);
if (user.SecuritySettings.TwoFactorEnabled)
{
string key = user.SecuritySettings.TwoFactorKey;

TimeAuthenticator ta = new TimeAuthenticator(usedCodeManager: _usedCodesManager);
bool isValid = ta.CheckCode(key, code, user);

if (isValid)
{
// the code was valid, let's log them in!
await SignInUser(user, returnUrl, rememberMe);

if (user.SecuritySettings.AllowTrustedDevices && rememberDevice)
{
// They want to remember the device, and have allow trusted devices on
var cookieOptions = UserHelper.CreateTrustedDeviceCookie(_config, user.Username, Request.Host.Host.GetDomain(), Request.IsLocal());
Response.Cookies.Append(Constants.TRUSTEDDEVICECOOKIE + "_" + username, cookieOptions.Item2, cookieOptions.Item1);

TrustedDevice device = new TrustedDevice();
device.UserId = user.UserId;
device.Name = (string.IsNullOrEmpty(deviceName)) ? "Unknown" : deviceName;
device.DateSeen = DateTime.Now;
device.Token = cookieOptions.Item2;

// Add the token
_dbContext.TrustedDevices.Add(device);
_dbContext.SaveChanges();
}

if (string.IsNullOrEmpty(returnUrl))
returnUrl = Request.Headers["Referer"].ToString();
return Json(new { result = returnUrl });
}
errorMessage = "Invalid Authentication Code" ;
}
errorMessage = "User does not have Two Factor Authentication enabled";
}
errorMessage = "User does not exist";

// Raise the error event
await _events.RaiseAsync(new UserLoginFailureEvent(username, errorMessage));
return Json(new { error = errorMessage });
}

public async Task SignInUser(User user, string returnUrl, bool rememberMe)
{
// Sign In with Identity
await _signInManager.SignInAsync(user, rememberMe);

// Sign in via Identity Server
await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.UserId.ToString(), user.Username));
}

public static async Task LogoutUser(ClaimsPrincipal user, HttpContext context, SignInManager<User> signInManager, IEventService eventService)
{
if (user?.Identity.IsAuthenticated == true)
{
// delete local authentication cookie
await signInManager.SignOutAsync();

// raise the logout event
await eventService.RaiseAsync(new UserLogoutSuccessEvent(user.GetSubjectId(), user.GetDisplayName()));
}
}

}
}

+ 24
- 0
Teknik/Areas/Accounts/ViewModels/LoginViewModel.cs View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Teknik.ViewModels;

namespace Teknik.Areas.Accounts.ViewModels
{
public class LoginViewModel : ViewModelBase
{
[Required]
[Display(Name = "Username")]
public string Username { get; set; }

[Required]
[Display(Name = "Password")]
[DataType(DataType.Password)]
public string Password { get; set; }
[Display(Name = "Remember Me")]
public bool RememberMe { get; set; }

public string ReturnUrl { get; set; }
}
}

+ 17
- 0
Teknik/Areas/Accounts/ViewModels/TwoFactorViewModel.cs View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Teknik.ViewModels;

namespace Teknik.Areas.Accounts.ViewModels
{
public class TwoFactorViewModel : ViewModelBase
{
public bool RememberMe { get; set; }

public string ReturnUrl { get; set; }

public bool AllowTrustedDevice { get; set; }
}
}

Teknik/Areas/User/Views/User/Login.cshtml → Teknik/Areas/Accounts/Views/Accounts/Login.cshtml View File

@@ -1,4 +1,4 @@
@model Teknik.Areas.Users.ViewModels.LoginViewModel
@model Teknik.Areas.Accounts.ViewModels.LoginViewModel

@if (Config.UserConfig.LoginEnabled)
{
@@ -12,7 +12,7 @@
</div>
</div>
</div>
<!form id="loginForm" action="@Url.SubRouteUrl("user", "User.Login")" method="post" accept-charset="UTF-8">
<!form id="loginForm" action="@Url.SubRouteUrl("accounts", "Accounts.Login")" method="post" accept-charset="UTF-8">
<input name="Login.ReturnUrl" id="loginReturnUrl" type="hidden" value="@Model.ReturnUrl" />
<div class="form-group">
<input type="text" class="form-control" id="loginUsername" value="" placeholder="Username" name="Login.Username" data-val-required="The Username field is required." data-val="true" />

Teknik/Areas/User/Views/User/TwoFactorCheck.cshtml → Teknik/Areas/Accounts/Views/Accounts/TwoFactorCheck.cshtml View File

@@ -1,7 +1,7 @@
@model Teknik.Areas.Users.ViewModels.TwoFactorViewModel
@model Teknik.Areas.Accounts.ViewModels.TwoFactorViewModel

<script>
var confirmAuthCodeURL = '@Url.SubRouteUrl("user", "User.Action", new { action = "ConfirmAuthenticatorCode" })';
var confirmAuthCodeURL = '@Url.SubRouteUrl("accounts", "Accounts.Action", new { action = "ConfirmAuthenticatorCode" })';
</script>

<div class="container">
@@ -46,4 +46,4 @@
</div>
</div>

<bundle src="js/user.checkAuthCode.min.js" append-version="true"></bundle>
<bundle src="js/accounts.checkAuthCode.min.js" append-version="true"></bundle>

Teknik/Areas/User/Views/User/ViewLogin.cshtml → Teknik/Areas/Accounts/Views/Accounts/ViewLogin.cshtml View File

@@ -1,4 +1,4 @@
@model Teknik.Areas.Users.ViewModels.LoginViewModel
@model Teknik.Areas.Accounts.ViewModels.LoginViewModel

<div class="container">
<div class="row">
@@ -6,7 +6,7 @@
<div class="text-center">
<h1>Teknik Login</h1>
<div class="col-md-4 col-md-offset-4">
@await Html.PartialAsync("../../Areas/User/Views/User/Login", Model)
@await Html.PartialAsync("../../Areas/Accounts/Views/Accounts/Login", Model)
</div>
</div>
</div>

+ 134
- 301
Teknik/Areas/User/Controllers/UserController.cs View File

@@ -24,6 +24,9 @@ using Teknik.Logging;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using Teknik.Areas.Accounts.Controllers;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Identity;

namespace Teknik.Areas.Users.Controllers
{
@@ -31,10 +34,17 @@ namespace Teknik.Areas.Users.Controllers
[Area("User")]
public class UserController : DefaultController
{
public UserController(ILogger<Logger> logger, Config config, TeknikEntities dbContext) : base(logger, config, dbContext) { }

private static readonly UsedCodesManager usedCodesManager = new UsedCodesManager();
private const string _AuthSessionKey = "AuthenticatedUser";

private readonly SignInManager<User> _signInManager;
private readonly IEventService _events;

public UserController(ILogger<Logger> logger, Config config, TeknikEntities dbContext, SignInManager<User> signInManager, IEventService eventService) : base(logger, config, dbContext)
{
_signInManager = signInManager;
_events = eventService;
}
[AllowAnonymous]
public IActionResult GetPremium()
@@ -46,6 +56,125 @@ namespace Teknik.Areas.Users.Controllers
return View(model);
}

[HttpGet]
[AllowAnonymous]
public IActionResult Register(string inviteCode, string ReturnUrl)
{
RegisterViewModel model = new RegisterViewModel();
model.InviteCode = inviteCode;
model.ReturnUrl = ReturnUrl;

return View("/Areas/User/Views/User/ViewRegistration.cshtml", model);
}

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Register([Bind(Prefix = "Register")]RegisterViewModel model)
{
model.Error = false;
model.ErrorMessage = string.Empty;
if (ModelState.IsValid)
{
if (_config.UserConfig.RegistrationEnabled)
{
if (!model.Error && !UserHelper.ValidUsername(_config, model.Username))
{
model.Error = true;
model.ErrorMessage = "That username is not valid";
}
if (!model.Error && !UserHelper.UsernameAvailable(_dbContext, _config, model.Username))
{
model.Error = true;
model.ErrorMessage = "That username is not available";
}
if (!model.Error && model.Password != model.ConfirmPassword)
{
model.Error = true;
model.ErrorMessage = "Passwords must match";
}

// Validate the Invite Code
if (!model.Error && _config.UserConfig.InviteCodeRequired && string.IsNullOrEmpty(model.InviteCode))
{
model.Error = true;
model.ErrorMessage = "An Invite Code is required to register";
}
if (!model.Error && !string.IsNullOrEmpty(model.InviteCode) && _dbContext.InviteCodes.Where(c => c.Code == model.InviteCode && c.Active && c.ClaimedUser == null).FirstOrDefault() == null)
{
model.Error = true;
model.ErrorMessage = "Invalid Invite Code";
}

// PGP Key valid?
if (!model.Error && !string.IsNullOrEmpty(model.PublicKey) && !PGP.IsPublicKey(model.PublicKey))
{
model.Error = true;
model.ErrorMessage = "Invalid PGP Public Key";
}

if (!model.Error)
{
try
{
User newUser = new User();
newUser.JoinDate = DateTime.Now;
newUser.Username = model.Username;
newUser.UserSettings = new UserSettings();
newUser.SecuritySettings = new SecuritySettings();
newUser.BlogSettings = new BlogSettings();
newUser.UploadSettings = new UploadSettings();

if (!string.IsNullOrEmpty(model.PublicKey))
newUser.SecuritySettings.PGPSignature = model.PublicKey;
if (!string.IsNullOrEmpty(model.RecoveryEmail))
newUser.SecuritySettings.RecoveryEmail = model.RecoveryEmail;

// if they provided an invite code, let's assign them to it
if (!string.IsNullOrEmpty(model.InviteCode))
{
InviteCode code = _dbContext.InviteCodes.Where(c => c.Code == model.InviteCode).FirstOrDefault();
_dbContext.Entry(code).State = EntityState.Modified;
_dbContext.SaveChanges();

newUser.ClaimedInviteCode = code;
}

UserHelper.AddAccount(_dbContext, _config, newUser, model.Password);

// If they have a recovery email, let's send a verification
if (!string.IsNullOrEmpty(model.RecoveryEmail))
{
string verifyCode = UserHelper.CreateRecoveryEmailVerification(_dbContext, _config, newUser);
string resetUrl = Url.SubRouteUrl("user", "User.ResetPassword", new { Username = model.Username });
string verifyUrl = Url.SubRouteUrl("user", "User.VerifyRecoveryEmail", new { Code = verifyCode });
UserHelper.SendRecoveryEmailVerification(_config, model.Username, model.RecoveryEmail, resetUrl, verifyUrl);
}
}
catch (Exception ex)
{
model.Error = true;
model.ErrorMessage = ex.GetFullMessage(true);
}
if (!model.Error)
{
return Redirect(Url.SubRouteUrl("accounts", "Accounts.Login", new { ReturnUrl = model.ReturnUrl }));
}
}
}
if (!model.Error)
{
model.Error = true;
model.ErrorMessage = "User Registration is Disabled";
}
}
else
{
model.Error = true;
model.ErrorMessage = "Missing Required Fields";
}
return GenerateActionResult(new { error = model.ErrorMessage }, View("/Areas/User/Views/User/ViewRegistration.cshtml", model));
}

// GET: Profile/Profile
[AllowAnonymous]
public IActionResult ViewProfile(string username)
@@ -291,235 +420,6 @@ namespace Teknik.Areas.Users.Controllers
return Redirect(Url.SubRouteUrl("error", "Error.Http404"));
}

[HttpGet]
[AllowAnonymous]
public IActionResult Login(string ReturnUrl)
{
LoginViewModel model = new LoginViewModel();
model.ReturnUrl = ReturnUrl;

return View("/Areas/User/Views/User/ViewLogin.cshtml", model);
}

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login([Bind(Prefix = "Login")]LoginViewModel model)
{
if (ModelState.IsValid)
{
string username = model.Username;
User user = UserHelper.GetUser(_dbContext, username);
if (user != null)
{
bool userValid = UserHelper.UserPasswordCorrect(_dbContext, _config, user, model.Password);
if (userValid)
{
// Perform transfer actions on the account
UserHelper.TransferUser(_dbContext, _config, user, model.Password);
user.LastSeen = DateTime.Now;
_dbContext.Entry(user).State = EntityState.Modified;
_dbContext.SaveChanges();

// Make sure they aren't banned or anything
if (user.AccountStatus == AccountStatus.Banned)
{
model.Error = true;
model.ErrorMessage = "Account has been banned.";

return GenerateActionResult(new { error = model.ErrorMessage }, View("/Areas/User/Views/User/ViewLogin.cshtml", model));
}

// Let's double check their email and git accounts to make sure they exist
string email = UserHelper.GetUserEmailAddress(_config, username);
if (_config.EmailConfig.Enabled && !UserHelper.UserEmailExists(_config, email))
{
UserHelper.AddUserEmail(_config, email, model.Password);
}

if (_config.GitConfig.Enabled && !UserHelper.UserGitExists(_config, username))
{
UserHelper.AddUserGit(_config, username, model.Password);
}

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
string token = Request.Cookies[Constants.TRUSTEDDEVICECOOKIE + "_" + username];
if (!string.IsNullOrEmpty(token))
{
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)
{
HttpContext.Session.Set(_AuthSessionKey, user.Username);
if (string.IsNullOrEmpty(model.ReturnUrl))
returnUrl = Request.Headers["Referer"].ToString();
returnUrl = Url.SubRouteUrl("user", "User.CheckAuthenticatorCode", new { returnUrl = returnUrl, rememberMe = model.RememberMe });
model.ReturnUrl = string.Empty;
}
else
{
returnUrl = Request.Headers["Referer"].ToString();
// They don't need two factor auth.
await SignInUser(user, (string.IsNullOrEmpty(model.ReturnUrl)) ? returnUrl : model.ReturnUrl, model.RememberMe);
}

if (string.IsNullOrEmpty(model.ReturnUrl))
{
return GenerateActionResult(new { result = returnUrl }, Redirect(returnUrl));
}
else
{
return Redirect(model.ReturnUrl);
}
}
}
}
model.Error = true;
model.ErrorMessage = "Invalid Username or Password.";

return GenerateActionResult(new { error = model.ErrorMessage }, View("/Areas/User/Views/User/ViewLogin.cshtml", model));
}

public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

return Redirect(Url.SubRouteUrl("www", "Home.Index"));
}

[HttpGet]
[AllowAnonymous]
public IActionResult Register(string inviteCode, string ReturnUrl)
{
RegisterViewModel model = new RegisterViewModel();
model.InviteCode = inviteCode;
model.ReturnUrl = ReturnUrl;

return View("/Areas/User/Views/User/ViewRegistration.cshtml", model);
}

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Register([Bind(Prefix = "Register")]RegisterViewModel model)
{
model.Error = false;
model.ErrorMessage = string.Empty;
if (ModelState.IsValid)
{
if (_config.UserConfig.RegistrationEnabled)
{
if (!model.Error && !UserHelper.ValidUsername(_config, model.Username))
{
model.Error = true;
model.ErrorMessage = "That username is not valid";
}
if (!model.Error && !UserHelper.UsernameAvailable(_dbContext, _config, model.Username))
{
model.Error = true;
model.ErrorMessage = "That username is not available";
}
if (!model.Error && model.Password != model.ConfirmPassword)
{
model.Error = true;
model.ErrorMessage = "Passwords must match";
}

// Validate the Invite Code
if (!model.Error && _config.UserConfig.InviteCodeRequired && string.IsNullOrEmpty(model.InviteCode))
{
model.Error = true;
model.ErrorMessage = "An Invite Code is required to register";
}
if (!model.Error && !string.IsNullOrEmpty(model.InviteCode) && _dbContext.InviteCodes.Where(c => c.Code == model.InviteCode && c.Active && c.ClaimedUser == null).FirstOrDefault() == null)
{
model.Error = true;
model.ErrorMessage = "Invalid Invite Code";
}

// PGP Key valid?
if (!model.Error && !string.IsNullOrEmpty(model.PublicKey) && !PGP.IsPublicKey(model.PublicKey))
{
model.Error = true;
model.ErrorMessage = "Invalid PGP Public Key";
}

if (!model.Error)
{
try
{
User newUser = new User();
newUser.JoinDate = DateTime.Now;
newUser.Username = model.Username;
newUser.UserSettings = new UserSettings();
newUser.SecuritySettings = new SecuritySettings();
newUser.BlogSettings = new BlogSettings();
newUser.UploadSettings = new UploadSettings();

if (!string.IsNullOrEmpty(model.PublicKey))
newUser.SecuritySettings.PGPSignature = model.PublicKey;
if (!string.IsNullOrEmpty(model.RecoveryEmail))
newUser.SecuritySettings.RecoveryEmail = model.RecoveryEmail;

// if they provided an invite code, let's assign them to it
if (!string.IsNullOrEmpty(model.InviteCode))
{
InviteCode code = _dbContext.InviteCodes.Where(c => c.Code == model.InviteCode).FirstOrDefault();
_dbContext.Entry(code).State = EntityState.Modified;
_dbContext.SaveChanges();

newUser.ClaimedInviteCode = code;
}

UserHelper.AddAccount(_dbContext, _config, newUser, model.Password);

// If they have a recovery email, let's send a verification
if (!string.IsNullOrEmpty(model.RecoveryEmail))
{
string verifyCode = UserHelper.CreateRecoveryEmailVerification(_dbContext, _config, newUser);
string resetUrl = Url.SubRouteUrl("user", "User.ResetPassword", new { Username = model.Username });
string verifyUrl = Url.SubRouteUrl("user", "User.VerifyRecoveryEmail", new { Code = verifyCode });
UserHelper.SendRecoveryEmailVerification(_config, model.Username, model.RecoveryEmail, resetUrl, verifyUrl);
}
}
catch (Exception ex)
{
model.Error = true;
model.ErrorMessage = ex.GetFullMessage(true);
}
if (!model.Error)
{
return await Login(new LoginViewModel { Username = model.Username, Password = model.Password, RememberMe = false, ReturnUrl = model.ReturnUrl });
}
}
}
if (!model.Error)
{
model.Error = true;
model.ErrorMessage = "User Registration is Disabled";
}
}
else
{
model.Error = true;
model.ErrorMessage = "Missing Required Fields";
}
return GenerateActionResult(new { error = model.ErrorMessage }, View("/Areas/User/Views/User/ViewRegistration.cshtml", model));
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult EditBlog(BlogSettingsViewModel settings)
@@ -741,8 +641,10 @@ namespace Teknik.Areas.Users.Controllers
if (user != null)
{
UserHelper.DeleteAccount(_dbContext, _config, user);

// Sign Out
await Logout();
await AccountsController.LogoutUser(User, HttpContext, _signInManager, _events);

return Json(new { result = true });
}
}
@@ -918,75 +820,6 @@ namespace Teknik.Areas.Users.Controllers
return Json(new { error = "Unable to reset user password" });
}

[HttpGet]
[AllowAnonymous]
public IActionResult ConfirmTwoFactorAuth(string returnUrl, bool rememberMe)
{
string username = HttpContext.Session.Get<string>(_AuthSessionKey);
if (!string.IsNullOrEmpty(username))
{
User user = UserHelper.GetUser(_dbContext, username);
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 Redirect(Url.SubRouteUrl("error", "Error.Http403"));
}

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ConfirmAuthenticatorCode(string code, string returnUrl, bool rememberMe, bool rememberDevice, string deviceName)
{
string username = HttpContext.Session.Get<string>(_AuthSessionKey);
if (!string.IsNullOrEmpty(username))
{
User user = UserHelper.GetUser(_dbContext, username);
if (user.SecuritySettings.TwoFactorEnabled)
{
string key = user.SecuritySettings.TwoFactorKey;

TimeAuthenticator ta = new TimeAuthenticator(usedCodeManager: usedCodesManager);
bool isValid = ta.CheckCode(key, code, user);

if (isValid)
{
// the code was valid, let's log them in!
await SignInUser(user, returnUrl, rememberMe);

if (user.SecuritySettings.AllowTrustedDevices && rememberDevice)
{
// They want to remember the device, and have allow trusted devices on
var cookieOptions = UserHelper.CreateTrustedDeviceCookie(_config, user.Username, Request.Host.Host.GetDomain(), Request.IsLocal());
Response.Cookies.Append(Constants.TRUSTEDDEVICECOOKIE + "_" + username, cookieOptions.Item2, cookieOptions.Item1);

TrustedDevice device = new TrustedDevice();
device.UserId = user.UserId;
device.Name = (string.IsNullOrEmpty(deviceName)) ? "Unknown" : deviceName;
device.DateSeen = DateTime.Now;
device.Token = cookieOptions.Item2;

// Add the token
_dbContext.TrustedDevices.Add(device);
_dbContext.SaveChanges();
}

if (string.IsNullOrEmpty(returnUrl))
returnUrl = Request.Headers["Referer"].ToString();
return Json(new { result = returnUrl });
}
return Json(new { error = "Invalid Authentication Code" });
}
return Json(new { error = "User does not have Two Factor Authentication enabled" });
}
return Json(new { error = "User does not exist" });
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult VerifyAuthenticatorCode(string code)

+ 22
- 0
Teknik/Areas/User/Models/LoginInfo.cs View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Teknik.Areas.Users.Models
{
public class LoginInfo
{
public int LoginInfoId { get; set; }

public virtual string LoginProvider { get; set; }

public virtual string ProviderDisplayName { get; set; }

public virtual string ProviderKey { get; set; }

public int UserId { get; set; }

public virtual User User { get; set; }
}
}

+ 8
- 1
Teknik/Areas/User/Models/User.cs View File

@@ -12,12 +12,17 @@ namespace Teknik.Areas.Users.Models
public class User
{
public int UserId { get; set; }
public string Username { get; set; }

[NotMapped]
public string Password { get; set; }

[CaseSensitive]
public string HashedPassword { get; set; }

public virtual ICollection<LoginInfo> Logins { get; set; }

public virtual ICollection<TransferType> Transfers { get; set; }

public DateTime JoinDate { get; set; }
@@ -57,7 +62,9 @@ namespace Teknik.Areas.Users.Models
public User()
{
Username = string.Empty;
Password = string.Empty;
HashedPassword = string.Empty;
Logins = new List<LoginInfo>();
Transfers = new List<TransferType>();
JoinDate = DateTime.Now;
LastSeen = DateTime.Now;

+ 1
- 1
Teknik/Areas/User/Views/User/_LoginModalPartial.cshtml View File

@@ -25,7 +25,7 @@
<h4 class="modal-title" id="loginModalLabel">Teknik Login</h4>
</div>
<div class="modal-body">
@await Html.PartialAsync("../../Areas/User/Views/User/Login", new Teknik.Areas.Users.ViewModels.LoginViewModel())
@await Html.PartialAsync("../../Areas/Accounts/Views/Accounts/Login", new Teknik.Areas.Accounts.ViewModels.LoginViewModel())
</div>
</div>
</div>

+ 2
- 2
Teknik/Areas/User/Views/User/_LoginPartial.cshtml View File

@@ -22,7 +22,7 @@
</li>
}
<li>
<a href="@Url.SubRouteUrl("user", "User.Logout")">Sign Out</a>
<a href="@Url.SubRouteUrl("accounts", "Accounts.Logout")">Sign Out</a>
</li>
</ul>
</li>
@@ -40,7 +40,7 @@
{
<button id="loginButton" data-toggle="modal" data-target="#loginModal" class="btn btn-default navbar-btn hide">Sign In</button>
<noscript>
<a href="@Url.SubRouteUrl("user", "User.Login")" class="btn btn-default navbar-btn">Sign In</a>
<a href="@Url.SubRouteUrl("accounts", "Accounts.Login")" class="btn btn-default navbar-btn">Sign In</a>
</noscript>
}
}

+ 26
- 1
Teknik/Attributes/TeknikAuthorizeAttribute.cs View File

@@ -11,6 +11,7 @@ using Teknik.Areas.Users.Models;
using Teknik.Configuration;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Teknik.Attributes
{
@@ -21,7 +22,7 @@ namespace Teknik.Attributes
}

[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public class TeknikAuthorizeAttribute : AuthorizeAttribute
public class TeknikAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
private AuthType m_AuthType { get; set; }

@@ -34,6 +35,30 @@ namespace Teknik.Attributes
m_AuthType = authType;
}

public void OnAuthorization(AuthorizationFilterContext context)
{
var user = context.HttpContext.User;

if (!user.Identity.IsAuthenticated)
{
// it isn't needed to set unauthorized result
// as the base class already requires the user to be authenticated
// this also makes redirect to a login page work properly
// context.Result = new UnauthorizedResult();
return;
}

//// you can also use registered services
//var someService = context.HttpContext.RequestServices.GetService<ISomeService>();

//var isAuthorized = someService.IsUserAuthorized(user.Identity.Name, _someFilterParameter);
//if (!isAuthorized)
//{
// context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
// return;
//}
}

//public override void OnAuthorization(AuthorizationContext filterContext)
//{
// if (filterContext == null)

+ 0
- 1
Teknik/Controllers/DefaultController.cs View File

@@ -24,7 +24,6 @@ using Teknik.Utilities;

namespace Teknik.Controllers
{
[AllowAnonymous]
[CORSActionFilter]
[Area("Default")]
public class DefaultController : Controller

+ 3
- 0
Teknik/Data/TeknikEntities.cs View File

@@ -19,6 +19,7 @@ namespace Teknik.Data
{
// Users
public DbSet<User> Users { get; set; }
public DbSet<LoginInfo> UserLogins { get; set; }
public DbSet<UserRole> UserRoles { get; set; }
public DbSet<Role> Roles { get; set; }
public DbSet<TrustedDevice> TrustedDevices { get; set; }
@@ -90,6 +91,7 @@ namespace Teknik.Data
modelBuilder.Entity<User>().HasMany(u => u.OwnedInviteCodes).WithOne(i => i.Owner);
modelBuilder.Entity<User>().HasMany(u => u.Transfers).WithOne(i => i.User);
modelBuilder.Entity<User>().HasOne(u => u.ClaimedInviteCode).WithOne(i => i.ClaimedUser);
modelBuilder.Entity<User>().HasMany(u => u.Logins).WithOne(r => r.User);
modelBuilder.Entity<User>().HasMany(u => u.UserRoles).WithOne(r => r.User);
modelBuilder.Entity<User>().HasOne(u => u.ClaimedInviteCode).WithOne(t => t.ClaimedUser); // Legacy???
modelBuilder.Entity<User>().HasMany(u => u.OwnedInviteCodes).WithOne(t => t.Owner); // Legacy???
@@ -181,6 +183,7 @@ namespace Teknik.Data

// Users
modelBuilder.Entity<User>().ToTable("Users");
modelBuilder.Entity<LoginInfo>().ToTable("UserLogins");
modelBuilder.Entity<UserRole>().ToTable("UserRoles");
modelBuilder.Entity<Role>().ToTable("Roles");
modelBuilder.Entity<TrustedDevice>().ToTable("TrustedDevices");

+ 86
- 0
Teknik/IdentityServerConfig.cs View File

@@ -0,0 +1,86 @@
using IdentityServer4;
using IdentityServer4.Configuration;
using IdentityServer4.Models;
using Microsoft.AspNetCore.Routing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Teknik
{
public class IdentityServerConfig
{
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api", "Teknik API")
};
}

public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}

public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "client",

// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ClientCredentials,

// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},

// scopes that client has access to
AllowedScopes = { "api" }
},
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,

RequireConsent = false,

ClientSecrets =
{
new Secret("secret".Sha256())
},

RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },

AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api"
},
AllowOfflineAccess = true
}
};
}

public static void SetupIdentityServer(IdentityServerOptions options)
{
RouteData routeData = new RouteData();
routeData.DataTokens.Add("area", "Error");
routeData.Values.Add("controller", "Error");
//routeData.Routers.Add(_router);
}
}
}

+ 1005
- 0
Teknik/Migrations/20180624191511_UserLoginInfo.Designer.cs
File diff suppressed because it is too large
View File


+ 40
- 0
Teknik/Migrations/20180624191511_UserLoginInfo.cs View File

@@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;

namespace Teknik.Migrations
{
public partial class UserLoginInfo : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "UserLogins",
columns: table => new
{
LoginInfoId = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
LoginProvider = table.Column<string>(nullable: true),
ProviderDisplayName = table.Column<string>(nullable: true),
ProviderKey = table.Column<string>(nullable: true),
UserId = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserLogins", x => x.LoginInfoId);
table.ForeignKey(
name: "FK_UserLogins_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
});
}

protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "UserLogins");
}
}
}

+ 1003
- 0
Teknik/Migrations/TeknikEntitiesModelSnapshot.cs
File diff suppressed because it is too large
View File


+ 33
- 21
Teknik/Routes.cs View File

@@ -15,6 +15,7 @@ namespace Teknik
routes.BuildDefaultRoutes(config);
routes.BuildAboutRoutes(config);
routes.BuildAbuseRoutes(config);
routes.BuildAccountsRoutes(config);
routes.BuildAdminRoutes(config);
routes.BuildAPIRoutes(config);
routes.BuildBlogRoutes(config);
@@ -91,6 +92,38 @@ namespace Teknik
);
}

public static void BuildAccountsRoutes(this IRouteBuilder routes, Config config)
{
routes.MapSubdomainRoute(
name: "Accounts.Login",
domains: new List<string>() { config.Host },
subDomains: new List<string>() { "accounts" },
template: "Login",
defaults: new { area = "Accounts", controller = "Accounts", action = "Login" }
);
routes.MapSubdomainRoute(
name: "Accounts.Logout",
domains: new List<string>() { config.Host },
subDomains: new List<string>() { "accounts" },
template: "Logout",
defaults: new { area = "Accounts", controller = "Accounts", action = "Logout" }
);
routes.MapSubdomainRoute(
name: "Accounts.CheckAuthenticatorCode",
domains: new List<string>() { config.Host },
subDomains: new List<string>() { "accounts" },
template: "CheckAuthCode",
defaults: new { area = "Accounts", controller = "Accounts", action = "ConfirmTwoFactorAuth" }
);
routes.MapSubdomainRoute(
name: "Accounts.Action",
domains: new List<string>() { config.Host },
subDomains: new List<string>() { "accounts" },
template: "Action/{action}",
defaults: new { area = "Accounts", controller = "Accounts", action = "Index" }
);
}

public static void BuildAdminRoutes(this IRouteBuilder routes, Config config)
{
routes.MapSubdomainRoute(
@@ -610,20 +643,6 @@ namespace Teknik
template: "GetPremium",
defaults: new { area = "User", controller = "User", action = "GetPremium" }
);
routes.MapSubdomainRoute(
name: "User.Login",
domains: new List<string>() { config.Host },
subDomains: new List<string>() { "user" },
template: "Login",
defaults: new { area = "User", controller = "User", action = "Login" }
);
routes.MapSubdomainRoute(
name: "User.Logout",
domains: new List<string>() { config.Host },
subDomains: new List<string>() { "user" },
template: "Logout",
defaults: new { area = "User", controller = "User", action = "Logout" }
);
routes.MapSubdomainRoute(
name: "User.Register",
domains: new List<string>() { config.Host },
@@ -694,13 +713,6 @@ namespace Teknik
template: "VerifyEmail/{code}",
defaults: new { area = "User", controller = "User", action = "VerifyRecoveryEmail" }
);
routes.MapSubdomainRoute(
name: "User.CheckAuthenticatorCode",
domains: new List<string>() { config.Host },
subDomains: new List<string>() { "user" },
template: "CheckAuthCode",
defaults: new { area = "User", controller = "User", action = "ConfirmTwoFactorAuth" }
);
routes.MapSubdomainRoute(
name: "User.ViewProfile",
domains: new List<string>() { config.Host },

+ 36
- 0
Teknik/Security/PasswordHasher.cs View File

@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Teknik.Areas.Users.Models;
using Teknik.Areas.Users.Utility;
using Teknik.Configuration;

namespace Teknik.Security
{
public class PasswordHasher : IPasswordHasher<User>
{
private readonly Config _config;

public PasswordHasher(Config config)
{
_config = config;
}

public string HashPassword(User user, string password)
{
return UserHelper.GeneratePassword(_config, user, password);
}

public PasswordVerificationResult VerifyHashedPassword(User user, string hashedPassword, string providedPassword)
{
var hashedProvidedPassword = UserHelper.GeneratePassword(_config, user, providedPassword);
if (hashedPassword == hashedProvidedPassword)
{
return PasswordVerificationResult.Success;
}
return PasswordVerificationResult.Failed;
}
}
}

Teknik/Areas/Accounts/ResourceOwnerPasswordValidator.cs → Teknik/Security/ResourceOwnerPasswordValidator.cs View File

@@ -8,7 +8,7 @@ using Teknik.Areas.Users.Utility;
using Teknik.Configuration;
using Teknik.Data;

namespace Teknik.Areas.Accounts
namespace Teknik.Security
{
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{

+ 91
- 0
Teknik/Security/RoleStore.cs View File

@@ -0,0 +1,91 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Teknik.Areas.Users.Models;
using Teknik.Configuration;
using Teknik.Data;

namespace Teknik.Security
{
public class RoleStore : IRoleStore<Role>
{
private readonly TeknikEntities _dbContext;
private readonly Config _config;

public RoleStore(TeknikEntities dbContext, Config config)
{
_dbContext = dbContext;
_config = config;
}

public async Task<IdentityResult> CreateAsync(Role role, CancellationToken cancellationToken)
{
await _dbContext.Roles.AddAsync(role);
await _dbContext.SaveChangesAsync();
return IdentityResult.Success;
}

public async Task<IdentityResult> DeleteAsync(Role role, CancellationToken cancellationToken)
{
_dbContext.Roles.Remove(role);
await _dbContext.SaveChangesAsync();
return IdentityResult.Success;
}

public async Task<Role> FindByIdAsync(string roleId, CancellationToken cancellationToken)
{
int id = int.Parse(roleId);
return _dbContext.Roles.Where(r => r.RoleId == id).FirstOrDefault();
}

public async Task<Role> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
{
return _dbContext.Roles.Where(r => r.Name == normalizedRoleName).FirstOrDefault();
}

public async Task<string> GetRoleIdAsync(Role role, CancellationToken cancellationToken)
{
return role.RoleId.ToString();
}

public async Task<string> GetRoleNameAsync(Role role, CancellationToken cancellationToken)
{
return role.Name;
}

public async Task<string> GetNormalizedRoleNameAsync(Role role, CancellationToken cancellationToken)
{
return role.Name;
}

public async Task SetNormalizedRoleNameAsync(Role role, string normalizedName, CancellationToken cancellationToken)
{
role.Name = normalizedName;
_dbContext.Entry(role).State = EntityState.Modified;
await _dbContext.SaveChangesAsync();
}

public async Task SetRoleNameAsync(Role role, string roleName, CancellationToken cancellationToken)
{
role.Name = roleName;
_dbContext.Entry(role).State = EntityState.Modified;
await _dbContext.SaveChangesAsync();
}

public async Task<IdentityResult> UpdateAsync(Role role, CancellationToken cancellationToken)
{
_dbContext.Entry(role).State = EntityState.Modified;
await _dbContext.SaveChangesAsync();
return IdentityResult.Success;
}

public void Dispose()
{
// Nothing to dispose
}
}
}

+ 60
- 0
Teknik/Security/SignInManager.cs View File

@@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Teknik.Areas.Users.Models;
using Teknik.Areas.Users.Utility;
using Teknik.Configuration;
using Teknik.Data;

namespace Teknik.Security
{
public class SignInManager : SignInManager<User>
{
private readonly UserManager<User> _userManager;
private readonly TeknikEntities _dbContext;
private readonly Config _config;

public SignInManager(
UserManager<User> userManager,
IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory<User> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
ILogger<SignInManager<User>> logger,
IAuthenticationSchemeProvider schemes,
TeknikEntities dbContext,
Config config)
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes)
{
_userManager = userManager;
_dbContext = dbContext;
_config = config;
}

public override async Task<SignInResult> PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure)
{
User user = UserHelper.GetUser(_dbContext, userName);
if (user != null)
{
return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure);
}
return SignInResult.Failed;
}

public override async Task<SignInResult> PasswordSignInAsync(User user, string password, bool isPersistent, bool lockoutOnFailure)
{
// Check to see if they are banned
if (user.AccountStatus == Utilities.AccountStatus.Banned)
{
return SignInResult.NotAllowed;
}

return await base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure);
}
}
}

+ 260
- 0
Teknik/Security/UserStore.cs View File

@@ -0,0 +1,260 @@
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Teknik.Areas.Users.Models;
using Teknik.Areas.Users.Utility;
using Teknik.Configuration;
using Teknik.Data;
using Teknik.Utilities;

namespace Teknik.Security
{
public class UserStore : IUserStore<User>, IUserLoginStore<User>, IUserClaimStore<User>, IUserPasswordStore<User>, IUserRoleStore<User>, IQueryableUserStore<User>
{
private readonly TeknikEntities _dbContext;
private readonly Config _config;

public IQueryable<User> Users => _dbContext.Users.AsQueryable();

public UserStore(TeknikEntities dbContext, Config config)
{
_dbContext = dbContext;
_config = config;
}

public void Dispose()
{
// Nothing to dispose
}

public async Task<IdentityResult> CreateAsync(User user, CancellationToken cancellationToken)
{
UserHelper.AddAccount(_dbContext, _config, user, user.Password);
return IdentityResult.Success;
}

public async Task<IdentityResult> DeleteAsync(User user, CancellationToken cancellationToken)
{
UserHelper.DeleteAccount(_dbContext, _config, user);
return IdentityResult.Success;
}

public async Task<User> FindByIdAsync(string userId, CancellationToken cancellationToken)
{
return UserHelper.GetUser(_dbContext, userId);
}

public async Task<User> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
{
return UserHelper.GetUser(_dbContext, normalizedUserName);
}

public async Task<string> GetNormalizedUserNameAsync(User user, CancellationToken cancellationToken)
{
return user.Username;
}

public async Task<string> GetUserIdAsync(User user, CancellationToken cancellationToken)
{
return user.Username;
}

public async Task<string> GetUserNameAsync(User user, CancellationToken cancellationToken)
{
return user.Username;
}

public async Task SetNormalizedUserNameAsync(User user, string normalizedName, CancellationToken cancellationToken)
{
user.Username = normalizedName;
UserHelper.EditUser(_dbContext, _config, user);
}

public async Task SetUserNameAsync(User user, string userName, CancellationToken cancellationToken)
{
user.Username = userName;
UserHelper.EditUser(_dbContext, _config, user);
}

public async Task<IdentityResult> UpdateAsync(User user, CancellationToken cancellationToken)
{
UserHelper.EditUser(_dbContext, _config, user);
return IdentityResult.Success;
}

// Password Store
public async Task SetPasswordHashAsync(User user, string passwordHash, CancellationToken cancellationToken)
{
user.HashedPassword = passwordHash;
UserHelper.EditUser(_dbContext, _config, user);
}

public async Task<string> GetPasswordHashAsync(User user, CancellationToken cancellationToken)
{
return user.HashedPassword;
}

public async Task<bool> HasPasswordAsync(User user, CancellationToken cancellationToken)
{
return !string.IsNullOrEmpty(user.HashedPassword);
}

// Role Store
public async Task AddToRoleAsync(User user, string roleName, CancellationToken cancellationToken)
{
var role = _dbContext.Roles.Where(r => r.Name == roleName).FirstOrDefault();
if (role == null)
throw new ArgumentException("Role does not exist", "roleName");

bool alreadyHasRole = await IsInRoleAsync(user, roleName, cancellationToken);
if (!alreadyHasRole)
{
UserRole userRole = new UserRole();
userRole.Role = role;
userRole.User = user;
await _dbContext.UserRoles.AddAsync(userRole);
await _dbContext.SaveChangesAsync();
}
}

public async Task RemoveFromRoleAsync(User user, string roleName, CancellationToken cancellationToken)
{
var userRoles = user.UserRoles.Where(r => r.Role.Name == roleName).ToList();
if (userRoles != null)
{
foreach (var userRole in userRoles)
{
_dbContext.UserRoles.Remove(userRole);
}
}
await _dbContext.SaveChangesAsync();
}

public async Task<IList<string>> GetRolesAsync(User user, CancellationToken cancellationToken)
{
return user.UserRoles.Select(ur => ur.Role.Name).ToList();
}

public async Task<bool> IsInRoleAsync(User user, string roleName, CancellationToken cancellationToken)
{
return UserHelper.UserHasRoles(user, roleName);
}

public async Task<IList<User>> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken)
{
var userRoles = _dbContext.UserRoles.Where(r => r.Role.Name == roleName).ToList();
if (userRoles != null)
{
return userRoles.Select(ur => ur.User).ToList();
}
return new List<User>();
}

// Login Info Store
public async Task AddLoginAsync(User user, UserLoginInfo login, CancellationToken cancellationToken)
{
LoginInfo info = new LoginInfo();
info.LoginProvider = login.LoginProvider;
info.ProviderDisplayName = login.ProviderDisplayName;
info.ProviderKey = login.ProviderKey;
info.User = user;

await _dbContext.UserLogins.AddAsync(info);
await _dbContext.SaveChangesAsync();
}

public async Task RemoveLoginAsync(User user, string loginProvider, string providerKey, CancellationToken cancellationToken)
{
var logins = user.Logins.Where(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey).ToList();
if (logins != null)
{
foreach (var login in logins)
{
_dbContext.UserLogins.Remove(login);
}
}
await _dbContext.SaveChangesAsync();
}

public async Task<IList<UserLoginInfo>> GetLoginsAsync(User user, CancellationToken cancellationToken)
{
List<UserLoginInfo> logins = new List<UserLoginInfo>();
foreach (var login in user.Logins)
{
UserLoginInfo info = new UserLoginInfo(login.LoginProvider, login.ProviderKey, login.ProviderDisplayName);
logins.Add(info);
}
return logins;
}

public async Task<User> FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
{
var foundLogin = _dbContext.UserLogins.Where(ul => ul.LoginProvider == loginProvider && ul.ProviderKey == providerKey).FirstOrDefault();
if (foundLogin != null)
{
return foundLogin.User;
}
return null;
}

// Claim Store
public async Task<IList<Claim>> GetClaimsAsync(User user, CancellationToken cancellationToken)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.Username)
};

// Add their roles
foreach (var role in user.UserRoles)
{
claims.Add(new Claim(ClaimTypes.Role, role.Role.Name));
}

return claims;
}

public async Task AddClaimsAsync(User user, IEnumerable<Claim> claims, CancellationToken cancellationToken)
{
foreach (var claim in claims)
{
if (claim.Type == ClaimTypes.Role)
{
await AddToRoleAsync(user, claim.Value, cancellationToken);
}
}
}

public async Task ReplaceClaimAsync(User user, Claim claim, Claim newClaim, CancellationToken cancellationToken)
{
// "no"
}

public async Task RemoveClaimsAsync(User user, IEnumerable<Claim> claims, CancellationToken cancellationToken)
{
// "no"
}

public async Task<IList<User>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken)
{
List<User> foundUsers = new List<User>();
if (claim.Type == ClaimTypes.Role)
{
var users = await GetUsersInRoleAsync(claim.Value, cancellationToken);
if (users != null && users.Any())
foundUsers.AddRange(users);
}
else if (claim.Type == ClaimTypes.Name)
{
var user = await FindByIdAsync(claim.Value, cancellationToken);
if (user != null)
foundUsers.Add(user);
}
return foundUsers;
}
}
}

+ 65
- 14
Teknik/Startup.cs View File

@@ -29,6 +29,7 @@ using Teknik.Security;
using Teknik.Attributes;
using Teknik.Filters;
using Microsoft.Net.Http.Headers;
using Teknik.Areas.Users.Models;

namespace Teknik
{
@@ -81,19 +82,72 @@ namespace Teknik
options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.None;
});

// Add Identity User
services.AddIdentity<User, Role>()
.AddUserStore<UserStore>()
.AddRoleStore<RoleStore>()
.AddDefaultTokenProviders();

services.AddTransient<IUserStore<User>, UserStore>();
services.AddTransient<IRoleStore<Role>, RoleStore>();
services.AddTransient<IPasswordHasher<User>, PasswordHasher>();

services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = "TeknikAuth";
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
options.Cookie.Expiration = TimeSpan.FromDays(30);
options.ExpireTimeSpan = TimeSpan.FromDays(30);
});

// Identity Server
services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
if (Environment.IsDevelopment())
{
options.UserInteraction.LoginUrl = new PathString("/Login?sub=user");
options.UserInteraction.ConsentUrl = new PathString("/Consent?sub=user");
}
else
{
options.UserInteraction.LoginUrl = new PathString("/User/User/Login");
options.UserInteraction.ConsentUrl = new PathString("/User/User/Consent");
}

// Setup Auth Cookies
options.Authentication.CheckSessionCookieName = "TeknikAuth";
})
.AddDeveloperSigningCredential()
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources())
.AddInMemoryApiResources(IdentityServerConfig.GetApiResources())
.AddInMemoryClients(IdentityServerConfig.GetClients())
.AddAspNetIdentity<User>();

// Setup Authentication Service
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
services.AddAuthentication()
.AddIdentityServerAuthentication(options =>
{
options.Cookie.Domain = null;
options.Cookie.Name = "TeknikAuth";
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
options.LoginPath = "/User/User/Login";
options.LogoutPath = "/User/User/Logout";
options.EventsType = typeof(TeknikCookieAuthenticationEvents);
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;

options.ApiName = "api";
})
.AddIdentityServerAuthentication("token", options =>
{
options.Authority = "http://localhost:5000";
options.ApiName = "api";

options.EnableCaching = true;
options.CacheDuration = TimeSpan.FromMinutes(10);
});
services.AddScoped<TeknikCookieAuthenticationEvents>();

// Compression Response
services.Configure<GzipCompressionProviderOptions>(options => options.Level = CompressionLevel.Fastest);
@@ -121,9 +175,6 @@ namespace Teknik

// Core MVC
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

//services.AddIdentityServer()
// .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -189,7 +240,7 @@ namespace Teknik
app.UseCookiePolicy();

// Authorize all the things!
app.UseAuthentication();
app.UseIdentityServer();

// And finally, let's use MVC
app.UseMvc(routes =>

+ 9
- 0
Teknik/Teknik.csproj View File

@@ -95,6 +95,15 @@
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Areas\Accounts\Views\Accounts\Login.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Areas\Accounts\Views\Accounts\TwoFactorCheck.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Areas\Accounts\Views\Accounts\ViewLogin.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
</ItemGroup>

<ItemGroup>

Loading…
Cancel
Save