login
This commit is contained in:
parent
5cf4bd48e8
commit
181a44e396
7 changed files with 198 additions and 82 deletions
1
.vscode/launch.json
vendored
1
.vscode/launch.json
vendored
|
|
@ -25,6 +25,7 @@
|
|||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/Org",
|
||||
"stopAtEntry": false,
|
||||
"justMyCode": false,
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ namespace Yavsc
|
|||
public const string UserNameRegExp = @"^[a-zA-Z][a-zA-Z0-9._-]*$";
|
||||
public const string UserFileNamePatternRegExp = @"^([a-zA-Z0-9._-]*/)*[a-zA-Z0-9._-]+$";
|
||||
|
||||
public const string LoginPath = "/signin";
|
||||
public const string LogoutPath = "/signout";
|
||||
public const string LoginPath = "~/signin";
|
||||
public const string LogoutPath = "~/signout";
|
||||
|
||||
public const string AccessDeniedPath = "/Account/AccessDenied";
|
||||
public const string AccessDeniedPath = "~/Account/AccessDenied";
|
||||
|
||||
public const string UserFilesPath = "/files";
|
||||
public const string AvatarsPath = "/avatars";
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ using Yavsc.ViewModels.Account;
|
|||
using Yavsc.Helpers;
|
||||
using Yavsc.Abstract.Manage;
|
||||
using Yavsc.Interface;
|
||||
using IdentityServer8.Test;
|
||||
using IdentityServer8.Services;
|
||||
using IdentityServer8.Stores;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
|
@ -22,14 +21,8 @@ using IdentityServer8.Models;
|
|||
using Yavsc.Extensions;
|
||||
using IdentityServer8.Events;
|
||||
using IdentityServer8.Extensions;
|
||||
using IdentityServer8;
|
||||
using IdentityModel;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Unicode;
|
||||
using System.Text;
|
||||
using Yavsc.Server.Helpers;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
|
||||
namespace Yavsc.Controllers
|
||||
{
|
||||
|
|
@ -104,15 +97,16 @@ namespace Yavsc.Controllers
|
|||
return RedirectToAction("Challenge", "External", new { scheme = vm.ExternalLoginScheme, returnUrl });
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
return View("Signin", vm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle postback from username/password login
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[HttpPost(Constants.LoginPath)]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Login(LoginInputModel model, string button)
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login([FromForm] SignInModel model, [FromForm] string button)
|
||||
{
|
||||
|
||||
// check if we are in the context of an authorization request
|
||||
|
|
@ -148,7 +142,7 @@ namespace Yavsc.Controllers
|
|||
if (ModelState.IsValid)
|
||||
{
|
||||
|
||||
var user = await _userManager.FindByNameAsync(model.Username);
|
||||
var user = await _userManager.FindByNameAsync(model.UserName);
|
||||
if (user != null)
|
||||
{
|
||||
|
||||
|
|
@ -162,7 +156,7 @@ namespace Yavsc.Controllers
|
|||
|
||||
// only set explicit expiration here if user chooses "remember me".
|
||||
// otherwise we rely upon expiration configured in cookie middleware.
|
||||
await HttpContext.SignInAsync(user, _roleManager, model.RememberLogin, _dbContext);
|
||||
await HttpContext.SignInAsync(user, _roleManager, model.RememberMe, _dbContext);
|
||||
|
||||
if (context != null)
|
||||
{
|
||||
|
|
@ -194,13 +188,13 @@ namespace Yavsc.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId: context?.Client.ClientId));
|
||||
await _events.RaiseAsync(new UserLoginFailureEvent(model.UserName, "invalid credentials", clientId: context?.Client.ClientId));
|
||||
ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
|
||||
}
|
||||
|
||||
// something went wrong, show form with error
|
||||
var vm = await BuildLoginViewModelAsync(model);
|
||||
return View(vm);
|
||||
return View("Signin", vm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -279,7 +273,7 @@ namespace Yavsc.Controllers
|
|||
/*****************************************/
|
||||
/* helper APIs for the AccountController */
|
||||
/*****************************************/
|
||||
private async Task<LoginViewModel> BuildLoginViewModelAsync(string returnUrl)
|
||||
private async Task<SignInModel> BuildLoginViewModelAsync(string returnUrl)
|
||||
{
|
||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null)
|
||||
|
|
@ -287,11 +281,12 @@ namespace Yavsc.Controllers
|
|||
var local = context.IdP == IdentityServer8.IdentityServerConstants.LocalIdentityProvider;
|
||||
|
||||
// this is meant to short circuit the UI and only trigger the one external IdP
|
||||
var vm = new LoginViewModel
|
||||
var vm = new SignInModel
|
||||
{
|
||||
EnableLocalLogin = local,
|
||||
ReturnUrl = returnUrl,
|
||||
Username = context?.LoginHint,
|
||||
UserName = context?.LoginHint,
|
||||
IsExternalLoginOnly = false
|
||||
};
|
||||
|
||||
if (!local)
|
||||
|
|
@ -327,21 +322,21 @@ namespace Yavsc.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
return new LoginViewModel
|
||||
return new SignInModel
|
||||
{
|
||||
AllowRememberLogin = AccountOptions.AllowRememberLogin,
|
||||
RememberMe = AccountOptions.AllowRememberLogin,
|
||||
EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin,
|
||||
ReturnUrl = returnUrl,
|
||||
Username = context?.LoginHint,
|
||||
UserName = context?.LoginHint,
|
||||
ExternalProviders = providers.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<LoginViewModel> BuildLoginViewModelAsync(LoginInputModel model)
|
||||
private async Task<SignInModel> BuildLoginViewModelAsync(SignInModel model)
|
||||
{
|
||||
var vm = await BuildLoginViewModelAsync(model.ReturnUrl);
|
||||
vm.Username = model.Username;
|
||||
vm.RememberLogin = model.RememberLogin;
|
||||
vm.UserName = model.UserName;
|
||||
vm.RememberMe = model.RememberMe;
|
||||
return vm;
|
||||
}
|
||||
|
||||
|
|
@ -448,6 +443,8 @@ namespace Yavsc.Controllers
|
|||
return View(new SignInModel
|
||||
{
|
||||
ReturnUrl = returnUrl ?? "/",
|
||||
ExternalProviders = [],
|
||||
EnableLocalLogin = true
|
||||
});
|
||||
/*
|
||||
Note: When using an external login provider, redirect the query :
|
||||
|
|
@ -469,8 +466,6 @@ namespace Yavsc.Controllers
|
|||
return View("AccessDenied", requestUrl);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost(Constants.LoginPath)]
|
||||
public async Task<IActionResult> SignIn(SignInModel model)
|
||||
{
|
||||
if (Request.Method == "POST") // "hGbkk9B94NAae#aG"
|
||||
|
|
@ -480,33 +475,63 @@ namespace Yavsc.Controllers
|
|||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||
var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, lockoutOnFailure: false);
|
||||
|
||||
if (result.Succeeded)
|
||||
var user = await _userManager.FindByNameAsync(model.UserName);
|
||||
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
|
||||
if (user != null)
|
||||
{
|
||||
// Redirect to returnUrl (ensure it's local to prevent open redirects)
|
||||
return LocalRedirect(model.ReturnUrl);
|
||||
|
||||
|
||||
var signin = await _signInManager.CheckPasswordSignInAsync(user, model.Password, true);
|
||||
|
||||
// validate username/password against in-memory store
|
||||
if (signin.Succeeded)
|
||||
{
|
||||
await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, clientId: context?.Client.ClientId));
|
||||
|
||||
// only set explicit expiration here if user chooses "remember me".
|
||||
// otherwise we rely upon expiration configured in cookie middleware.
|
||||
await HttpContext.SignInAsync(user, _roleManager, model.RememberMe, _dbContext);
|
||||
var authResult = await HttpContext.AuthenticateAsync();
|
||||
if (!authResult.Succeeded)
|
||||
{
|
||||
return this.Unauthorized();
|
||||
}
|
||||
String bearer = await HttpContext.GetTokenAsync("Bearer", "Bearer");
|
||||
HttpContext.Response.Cookies.Append("Bearer", bearer);
|
||||
|
||||
if (context != null)
|
||||
{
|
||||
if (context.IsNativeClient())
|
||||
{
|
||||
// The client is native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return this.LoadingPage("Redirect", model.ReturnUrl);
|
||||
}
|
||||
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
return RedirectToAction(nameof(SendCode), new { returnUrl = model.ReturnUrl, rememberMe = model.RememberMe });
|
||||
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
|
||||
return Redirect(model.ReturnUrl);
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
|
||||
// request for a local page
|
||||
if (Url.IsLocalUrl(model.ReturnUrl))
|
||||
{
|
||||
_logger.LogWarning(2, "User account locked out.");
|
||||
return this.ViewOk("Lockout");
|
||||
return Redirect(model.ReturnUrl);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(model.ReturnUrl))
|
||||
{
|
||||
return Redirect("~/");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, $"Invalid login attempt. ({model.UserName}, {model.Password})");
|
||||
return this.ViewOk(model);
|
||||
// user might have clicked on a malicious link - should be logged
|
||||
throw new Exception("invalid return URL");
|
||||
}
|
||||
}
|
||||
// If we got this far, something failed, redisplay form
|
||||
ModelState.AddModelError(string.Empty, "Unexpected behavior: something failed ... you could try again, or contact me ...");
|
||||
}
|
||||
|
||||
await _events.RaiseAsync(new UserLoginFailureEvent(model.UserName, "invalid credentials", clientId: context?.Client.ClientId));
|
||||
ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -123,7 +123,15 @@ public static class HostingExtensions
|
|||
services.AddTransient<IExternalIdentityManager, ExternalIdentityManager>();
|
||||
|
||||
|
||||
AddAuthentication(builder);
|
||||
services.AddAuthentication("Bearer")
|
||||
.AddJwtBearer("Bearer", options =>
|
||||
{
|
||||
options.IncludeErrorDetails = true;
|
||||
options.Authority = builder.Configuration.GetSection("Site")["Authority"];
|
||||
options.TokenValidationParameters =
|
||||
new() { ValidateAudience = false, RoleClaimType = Constants.RoleClaimType };
|
||||
options.MapInboundClaims = true;
|
||||
});
|
||||
|
||||
services.AddTransient<RoleManager<IdentityRole>>();
|
||||
services.AddTransient<IRoleStore<IdentityRole>, RoleStore<IdentityRole, ApplicationDbContext>>();
|
||||
|
|
@ -199,6 +207,7 @@ public static class HostingExtensions
|
|||
public static IServiceCollection LoadConfiguration(this WebApplicationBuilder builder)
|
||||
{
|
||||
var siteSection = builder.Configuration.GetSection("Site");
|
||||
|
||||
var smtpSection = builder.Configuration.GetSection("Smtp");
|
||||
var paypalSection = builder.Configuration.GetSection("Authentication:PayPal");
|
||||
// OAuth2AppSettings
|
||||
|
|
@ -276,19 +285,6 @@ public static class HostingExtensions
|
|||
sql => sql.MigrationsAssembly(migrationsAssembly));
|
||||
});
|
||||
|
||||
builder.Services.AddAuthentication(
|
||||
CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(options =>
|
||||
{
|
||||
options.LoginPath = Constants.LoginPath; // Redirect here if unauthenticated
|
||||
options.AccessDeniedPath = Constants.AccessDeniedPath;
|
||||
options.Cookie.SecurePolicy = builder.Environment.IsDevelopment()
|
||||
? CookieSecurePolicy.None
|
||||
: CookieSecurePolicy.Always; // Use HTTPS in production
|
||||
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax; // Allows cross-site top-level navigation
|
||||
options.ExpireTimeSpan = TimeSpan.FromMinutes(30); // Cookie expires in 30 mins
|
||||
options.SlidingExpiration = true; // Renew cookie if user is active
|
||||
});
|
||||
|
||||
builder.Services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
|
|
|
|||
87
src/Org/Views/Account/Signin.cshtml
Normal file
87
src/Org/Views/Account/Signin.cshtml
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
@model SignInModel
|
||||
|
||||
<div class="login-page">
|
||||
<div class="lead">
|
||||
<h1>Login</h1>
|
||||
<p>Choose how to login</p>
|
||||
</div>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
|
||||
@if (Model.EnableLocalLogin)
|
||||
{
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Local Account</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form asp-action="Login" method="POST" asp-controller="Account">
|
||||
<input type="hidden" asp-for="ReturnUrl" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="UserName"></label>
|
||||
<input class="form-control" placeholder="Username" asp-for="UserName" autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Password"></label>
|
||||
<input type="password" class="form-control" placeholder="Password" asp-for="Password" autocomplete="off">
|
||||
</div>
|
||||
@if (Model.AllowRememberLogin)
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" asp-for="RememberMe">
|
||||
<label class="form-check-label" asp-for="RememberMe">
|
||||
Remember My Login
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<button class="btn btn-primary" name="button" value="login">Login</button>
|
||||
<button class="btn btn-secondary" name="button" value="cancel">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.ExternalProviders.Any())
|
||||
{
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>External Account</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-inline">
|
||||
@foreach (var provider in Model.ExternalProviders)
|
||||
{
|
||||
<li class="list-inline-item">
|
||||
<a class="btn btn-secondary"
|
||||
asp-controller="External"
|
||||
asp-action="Challenge"
|
||||
asp-route-scheme="@provider.AuthenticationScheme"
|
||||
asp-route-returnUrl="@Model.ReturnUrl">
|
||||
@provider.DisplayName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!Model.EnableLocalLogin && !Model.ExternalProviders.Any())
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<strong>Invalid login request</strong>
|
||||
There are no login schemes configured for this request.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.InteropServices;
|
||||
using Yavsc.Attributes.Validation;
|
||||
|
||||
namespace Yavsc.ViewModels.Account
|
||||
|
|
@ -20,7 +21,7 @@ namespace Yavsc.ViewModels.Account
|
|||
/// Local user's password .
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[YaRequired]
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
|
|
@ -37,7 +38,7 @@ namespace Yavsc.ViewModels.Account
|
|||
/// and user password credentials.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string Provider { get; set; }
|
||||
public string? Provider { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -45,14 +46,20 @@ namespace Yavsc.ViewModels.Account
|
|||
/// but the one called once authorized.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string ReturnUrl { get; set; }
|
||||
public string? ReturnUrl { get; set; }
|
||||
public bool EnableLocalLogin { get; set; }
|
||||
public ExternalProvider[]? ExternalProviders { get; set; }
|
||||
public bool IsExternalLoginOnly { get; set; }
|
||||
public String? ExternalLoginScheme { get; set; }
|
||||
public bool AllowRememberLogin { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class YaAuthenticationDescription {
|
||||
public class YaAuthenticationDescription
|
||||
{
|
||||
public string DisplayName { get; set; }
|
||||
public string AuthenticationScheme { get; set; }
|
||||
|
||||
public IDictionary<string,object> Items { get; set; }
|
||||
public IDictionary<string, object> Items { get; set; }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue