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:
parent
50e16d87cb
commit
7ec30d1e39
@ -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
Teknik/Areas/Accounts/Controllers/AccountsController.cs
Normal file
285
Teknik/Areas/Accounts/Controllers/AccountsController.cs
Normal 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
Teknik/Areas/Accounts/ViewModels/LoginViewModel.cs
Normal file
24
Teknik/Areas/Accounts/ViewModels/LoginViewModel.cs
Normal 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
Teknik/Areas/Accounts/ViewModels/TwoFactorViewModel.cs
Normal file
17
Teknik/Areas/Accounts/ViewModels/TwoFactorViewModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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" />
|
@ -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>
|
@ -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>
|
@ -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
Teknik/Areas/User/Models/LoginInfo.cs
Normal file
22
Teknik/Areas/User/Models/LoginInfo.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -24,7 +24,6 @@ using Teknik.Utilities;
|
||||
|
||||
namespace Teknik.Controllers
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[CORSActionFilter]
|
||||
[Area("Default")]
|
||||
public class DefaultController : Controller
|
||||
|
@ -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
Teknik/IdentityServerConfig.cs
Normal file
86
Teknik/IdentityServerConfig.cs
Normal 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
Teknik/Migrations/20180624191511_UserLoginInfo.Designer.cs
generated
Normal file
1005
Teknik/Migrations/20180624191511_UserLoginInfo.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
Teknik/Migrations/20180624191511_UserLoginInfo.cs
Normal file
40
Teknik/Migrations/20180624191511_UserLoginInfo.cs
Normal 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
Teknik/Migrations/TeknikEntitiesModelSnapshot.cs
Normal file
1003
Teknik/Migrations/TeknikEntitiesModelSnapshot.cs
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
Teknik/Security/PasswordHasher.cs
Normal file
36
Teknik/Security/PasswordHasher.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
Teknik/Security/RoleStore.cs
Normal file
91
Teknik/Security/RoleStore.cs
Normal 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
Teknik/Security/SignInManager.cs
Normal file
60
Teknik/Security/SignInManager.cs
Normal 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
Teknik/Security/UserStore.cs
Normal file
260
Teknik/Security/UserStore.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 =>
|
||||
|
@ -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…
Reference in New Issue
Block a user