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

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

This commit is contained in:
Uncled1023 2018-06-24 21:23:04 -07:00
parent 50e16d87cb
commit 7ec30d1e39
27 changed files with 3226 additions and 351 deletions

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)

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()));
}
}
}
}

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; }
}
}

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; }
}
}

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" />

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>

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>

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)

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; }
}
}

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;

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>

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>
}
}

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)

View File

@ -24,7 +24,6 @@ using Teknik.Utilities;
namespace Teknik.Controllers
{
[AllowAnonymous]
[CORSActionFilter]
[Area("Default")]
public class DefaultController : Controller

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");

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);
}
}
}

File diff suppressed because it is too large Load Diff

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");
}
}
}

File diff suppressed because it is too large Load Diff

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 },

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;
}
}
}

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
{

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
}
}
}

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);
}
}
}

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;
}
}
}

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;
});
// Setup Authentication Service
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
// 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.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.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()
.AddIdentityServerAuthentication(options =>
{
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 =>

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>