From 6a817d84afa85ed789a56b8335412ddf0e50b7c8 Mon Sep 17 00:00:00 2001 From: Alexander Kulik Date: Thu, 25 Jul 2019 15:52:47 +0300 Subject: [PATCH 1/6] Added identity scaffolds. --- src/Areas/Identity/IdentityHostingStartup.cs | 22 +++ .../Pages/Account/ExternalLogin.cshtml | 33 ++++ .../Pages/Account/ExternalLogin.cshtml.cs | 141 ++++++++++++++++++ src/Areas/Identity/Pages/Account/Login.cshtml | 82 ++++++++++ .../Identity/Pages/Account/Login.cshtml.cs | 103 +++++++++++++ .../Pages/Account/Manage/ManageNavPages.cs | 35 +++++ .../Pages/Account/Manage/_ManageNav.cshtml | 14 ++ .../Pages/Account/Manage/_ViewImports.cshtml | 1 + .../Pages/Account/_ViewImports.cshtml | 1 + .../Pages/_ValidationScriptsPartial.cshtml | 18 +++ src/Areas/Identity/Pages/_ViewImports.cshtml | 5 + src/Areas/Identity/Pages/_ViewStart.cshtml | 4 + src/IdentityServer4WebApp.csproj | 2 + src/Pages/Shared/_CookieConsentPartial.cshtml | 25 ++++ src/Pages/_ViewStart.cshtml | 3 + src/ScaffoldingReadme.txt | 18 +++ 16 files changed, 507 insertions(+) create mode 100644 src/Areas/Identity/IdentityHostingStartup.cs create mode 100644 src/Areas/Identity/Pages/Account/ExternalLogin.cshtml create mode 100644 src/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs create mode 100644 src/Areas/Identity/Pages/Account/Login.cshtml create mode 100644 src/Areas/Identity/Pages/Account/Login.cshtml.cs create mode 100644 src/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs create mode 100644 src/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml create mode 100644 src/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml create mode 100644 src/Areas/Identity/Pages/Account/_ViewImports.cshtml create mode 100644 src/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml create mode 100644 src/Areas/Identity/Pages/_ViewImports.cshtml create mode 100644 src/Areas/Identity/Pages/_ViewStart.cshtml create mode 100644 src/Pages/Shared/_CookieConsentPartial.cshtml create mode 100644 src/Pages/_ViewStart.cshtml create mode 100644 src/ScaffoldingReadme.txt diff --git a/src/Areas/Identity/IdentityHostingStartup.cs b/src/Areas/Identity/IdentityHostingStartup.cs new file mode 100644 index 0000000..d46ea65 --- /dev/null +++ b/src/Areas/Identity/IdentityHostingStartup.cs @@ -0,0 +1,22 @@ +using System; +using IdentityServer4WebApp.Data; +using IdentityServer4WebApp.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +[assembly: HostingStartup(typeof(IdentityServer4WebApp.Areas.Identity.IdentityHostingStartup))] +namespace IdentityServer4WebApp.Areas.Identity +{ + public class IdentityHostingStartup : IHostingStartup + { + public void Configure(IWebHostBuilder builder) + { + builder.ConfigureServices((context, services) => { + }); + } + } +} \ No newline at end of file diff --git a/src/Areas/Identity/Pages/Account/ExternalLogin.cshtml b/src/Areas/Identity/Pages/Account/ExternalLogin.cshtml new file mode 100644 index 0000000..6058974 --- /dev/null +++ b/src/Areas/Identity/Pages/Account/ExternalLogin.cshtml @@ -0,0 +1,33 @@ +@page +@model ExternalLoginModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"]

+

Associate your @Model.LoginProvider account.

+
+ +

+ You've successfully authenticated with @Model.LoginProvider. + Please enter an email address for this site below and click the Register button to finish + logging in. +

+ +
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/src/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs b/src/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs new file mode 100644 index 0000000..3a5d8d3 --- /dev/null +++ b/src/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using IdentityServer4WebApp.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace IdentityServer4WebApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ExternalLoginModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public ExternalLoginModel( + SignInManager signInManager, + UserManager userManager, + ILogger logger) + { + _signInManager = signInManager; + _userManager = userManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public string LoginProvider { get; set; } + + public string ReturnUrl { get; set; } + + [TempData] + public string ErrorMessage { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + } + + public IActionResult OnGetAsync() + { + return RedirectToPage("./Login"); + } + + public IActionResult OnPost(string provider, string returnUrl = null) + { + // Request a redirect to the external login provider. + var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl }); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + return new ChallengeResult(provider, properties); + } + + public async Task OnGetCallbackAsync(string returnUrl = null, string remoteError = null) + { + returnUrl = returnUrl ?? Url.Content("~/"); + if (remoteError != null) + { + ErrorMessage = $"Error from external provider: {remoteError}"; + return RedirectToPage("./Login", new {ReturnUrl = returnUrl }); + } + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + ErrorMessage = "Error loading external login information."; + return RedirectToPage("./Login", new { ReturnUrl = returnUrl }); + } + + // Sign in the user with this external login provider if the user already has a login. + var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor : true); + if (result.Succeeded) + { + _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider); + return LocalRedirect(returnUrl); + } + if (result.IsLockedOut) + { + return RedirectToPage("./Lockout"); + } + else + { + // If the user does not have an account, then ask the user to create an account. + ReturnUrl = returnUrl; + LoginProvider = info.LoginProvider; + if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email)) + { + Input = new InputModel + { + Email = info.Principal.FindFirstValue(ClaimTypes.Email) + }; + } + return Page(); + } + } + + public async Task OnPostConfirmationAsync(string returnUrl = null) + { + returnUrl = returnUrl ?? Url.Content("~/"); + // Get the information about the user from the external login provider + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + ErrorMessage = "Error loading external login information during confirmation."; + return RedirectToPage("./Login", new { ReturnUrl = returnUrl }); + } + + if (ModelState.IsValid) + { + var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email }; + var result = await _userManager.CreateAsync(user); + if (result.Succeeded) + { + result = await _userManager.AddLoginAsync(user, info); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider); + return LocalRedirect(returnUrl); + } + } + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + LoginProvider = info.LoginProvider; + ReturnUrl = returnUrl; + return Page(); + } + } +} diff --git a/src/Areas/Identity/Pages/Account/Login.cshtml b/src/Areas/Identity/Pages/Account/Login.cshtml new file mode 100644 index 0000000..d998362 --- /dev/null +++ b/src/Areas/Identity/Pages/Account/Login.cshtml @@ -0,0 +1,82 @@ +@page +@model LoginModel + +@{ + ViewData["Title"] = "Log in"; +} + +

@ViewData["Title"]

+
+
+
+
+

Use a local account to log in.

+
+
+
+ + + +
+
+ + + +
+
+
+ +
+
+
+ +
+ +
+
+
+
+
+

Use another service to log in.

+
+ @{ + if ((Model.ExternalLogins?.Count ?? 0) == 0) + { +
+

+ There are no external authentication services configured. See this article + for details on setting up this ASP.NET application to support logging in via external services. +

+
+ } + else + { +
+
+

+ @foreach (var provider in Model.ExternalLogins) + { + + } +

+
+
+ } + } +
+
+
+ +@section Scripts { + +} diff --git a/src/Areas/Identity/Pages/Account/Login.cshtml.cs b/src/Areas/Identity/Pages/Account/Login.cshtml.cs new file mode 100644 index 0000000..0005f23 --- /dev/null +++ b/src/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using IdentityServer4WebApp.Models; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace IdentityServer4WebApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LoginModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public IList ExternalLogins { get; set; } + + public string ReturnUrl { get; set; } + + [TempData] + public string ErrorMessage { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } + + public async Task OnGetAsync(string returnUrl = null) + { + if (!string.IsNullOrEmpty(ErrorMessage)) + { + ModelState.AddModelError(string.Empty, ErrorMessage); + } + + returnUrl = returnUrl ?? Url.Content("~/"); + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + + ReturnUrl = returnUrl; + } + + public async Task OnPostAsync(string returnUrl = null) + { + returnUrl = returnUrl ?? Url.Content("~/"); + + 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(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true); + if (result.Succeeded) + { + _logger.LogInformation("User logged in."); + return LocalRedirect(returnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); + } + if (result.IsLockedOut) + { + _logger.LogWarning("User account locked out."); + return RedirectToPage("./Lockout"); + } + else + { + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return Page(); + } + } + + // If we got this far, something failed, redisplay form + return Page(); + } + } +} diff --git a/src/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs b/src/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs new file mode 100644 index 0000000..c8a84bf --- /dev/null +++ b/src/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace IdentityServer4WebApp.Areas.Identity.Pages.Account.Manage +{ + public static class ManageNavPages + { + public static string Index => "Index"; + + public static string ChangePassword => "ChangePassword"; + + public static string ExternalLogins => "ExternalLogins"; + + public static string PersonalData => "PersonalData"; + + public static string TwoFactorAuthentication => "TwoFactorAuthentication"; + + public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); + + public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); + + public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); + + public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData); + + public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); + + private static string PageNavClass(ViewContext viewContext, string page) + { + var activePage = viewContext.ViewData["ActivePage"] as string + ?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName); + return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; + } + } +} \ No newline at end of file diff --git a/src/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml b/src/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml new file mode 100644 index 0000000..677b43e --- /dev/null +++ b/src/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml @@ -0,0 +1,14 @@ +@inject SignInManager SignInManager +@{ + var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); +} + diff --git a/src/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml b/src/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml new file mode 100644 index 0000000..d75b855 --- /dev/null +++ b/src/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml @@ -0,0 +1 @@ +@using IdentityServer4WebApp.Areas.Identity.Pages.Account.Manage diff --git a/src/Areas/Identity/Pages/Account/_ViewImports.cshtml b/src/Areas/Identity/Pages/Account/_ViewImports.cshtml new file mode 100644 index 0000000..ae330dd --- /dev/null +++ b/src/Areas/Identity/Pages/Account/_ViewImports.cshtml @@ -0,0 +1 @@ +@using IdentityServer4WebApp.Areas.Identity.Pages.Account \ No newline at end of file diff --git a/src/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml b/src/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..9e26f3b --- /dev/null +++ b/src/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/src/Areas/Identity/Pages/_ViewImports.cshtml b/src/Areas/Identity/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..636114b --- /dev/null +++ b/src/Areas/Identity/Pages/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Identity +@using IdentityServer4WebApp.Areas.Identity +@using IdentityServer4WebApp.Models +@namespace IdentityServer4WebApp.Areas.Identity.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/Areas/Identity/Pages/_ViewStart.cshtml b/src/Areas/Identity/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..2909abd --- /dev/null +++ b/src/Areas/Identity/Pages/_ViewStart.cshtml @@ -0,0 +1,4 @@ + +@{ + Layout = "/Pages/Shared/_Layout.cshtml"; +} diff --git a/src/IdentityServer4WebApp.csproj b/src/IdentityServer4WebApp.csproj index d4cbc74..2314b3d 100644 --- a/src/IdentityServer4WebApp.csproj +++ b/src/IdentityServer4WebApp.csproj @@ -33,9 +33,11 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all + \ No newline at end of file diff --git a/src/Pages/Shared/_CookieConsentPartial.cshtml b/src/Pages/Shared/_CookieConsentPartial.cshtml new file mode 100644 index 0000000..d7a5718 --- /dev/null +++ b/src/Pages/Shared/_CookieConsentPartial.cshtml @@ -0,0 +1,25 @@ +@using Microsoft.AspNetCore.Http.Features + +@{ + var consentFeature = Context.Features.Get(); + var showBanner = !consentFeature?.CanTrack ?? false; + var cookieString = consentFeature?.CreateConsentCookie(); +} + +@if (showBanner) +{ + + +} \ No newline at end of file diff --git a/src/Pages/_ViewStart.cshtml b/src/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..e25d5d0 --- /dev/null +++ b/src/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; + } diff --git a/src/ScaffoldingReadme.txt b/src/ScaffoldingReadme.txt new file mode 100644 index 0000000..da8a278 --- /dev/null +++ b/src/ScaffoldingReadme.txt @@ -0,0 +1,18 @@ +Support for ASP.NET Core Identity was added to your project +- The code for adding Identity to your project was generated under Areas/Identity. + +Configuration of the Identity related services can be found in the Areas/Identity/IdentityHostingStartup.cs file. + + +The generated UI requires support for static files. To add static files to your app: +1. Call app.UseStaticFiles() from your Configure method + +To use ASP.NET Core Identity you also need to enable authentication. To authentication to your app: +1. Call app.UseAuthentication() from your Configure method (after static files) + +The generated UI requires MVC. To add MVC to your app: +1. Call services.AddMvc() from your ConfigureServices method +2. Call app.UseMvc() from your Configure method (after authentication) + +Apps that use ASP.NET Core Identity should also use HTTPS. To enable HTTPS see https://go.microsoft.com/fwlink/?linkid=848054. + From 65f5e14bb8af8c4eea4a85b5f044b961d600d534 Mon Sep 17 00:00:00 2001 From: Alexander Kulik Date: Thu, 25 Jul 2019 15:55:41 +0300 Subject: [PATCH 2/6] Added login by windows button. --- src/Areas/Identity/Pages/Account/Login.cshtml | 2 +- src/Areas/Identity/Pages/Account/Login.cshtml.cs | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Areas/Identity/Pages/Account/Login.cshtml b/src/Areas/Identity/Pages/Account/Login.cshtml index d998362..e9963ad 100644 --- a/src/Areas/Identity/Pages/Account/Login.cshtml +++ b/src/Areas/Identity/Pages/Account/Login.cshtml @@ -66,7 +66,7 @@

@foreach (var provider in Model.ExternalLogins) { - + }

diff --git a/src/Areas/Identity/Pages/Account/Login.cshtml.cs b/src/Areas/Identity/Pages/Account/Login.cshtml.cs index 0005f23..be22cac 100644 --- a/src/Areas/Identity/Pages/Account/Login.cshtml.cs +++ b/src/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -9,20 +9,24 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Server.IISIntegration; using Microsoft.Extensions.Logging; namespace IdentityServer4WebApp.Areas.Identity.Pages.Account { - [AllowAnonymous] + [Authorize(AuthenticationSchemes = "Windows")] public class LoginModel : PageModel { private readonly SignInManager _signInManager; private readonly ILogger _logger; - public LoginModel(SignInManager signInManager, ILogger logger) + private readonly IAuthenticationSchemeProvider _schemeProvider; + + public LoginModel(SignInManager signInManager, ILogger logger, IAuthenticationSchemeProvider schemeProvider) { _signInManager = signInManager; _logger = logger; + _schemeProvider = schemeProvider; } [BindProperty] @@ -61,7 +65,7 @@ public async Task OnGetAsync(string returnUrl = null) // Clear the existing external cookie to ensure a clean login process await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); - ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + ExternalLogins = (await _schemeProvider.GetAllSchemesAsync()).Where(x => x.DisplayName != null || (x.Name.Equals(IISDefaults.AuthenticationScheme, StringComparison.OrdinalIgnoreCase))).ToList(); ReturnUrl = returnUrl; } From 60fbd44cb8060025558d96835505719598625f8b Mon Sep 17 00:00:00 2001 From: Alexander Kulik Date: Thu, 25 Jul 2019 15:58:18 +0300 Subject: [PATCH 3/6] Changed externallogin page. --- .../Pages/Account/ExternalLogin.cshtml.cs | 71 ++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/src/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs b/src/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs index 3a5d8d3..88eb4de 100644 --- a/src/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs +++ b/src/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs @@ -3,12 +3,16 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Security.Claims; +using System.Security.Principal; using System.Threading.Tasks; +using IdentityModel; using Microsoft.AspNetCore.Authorization; using IdentityServer4WebApp.Models; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Server.IISIntegration; using Microsoft.Extensions.Logging; namespace IdentityServer4WebApp.Areas.Identity.Pages.Account @@ -52,9 +56,13 @@ public IActionResult OnGetAsync() return RedirectToPage("./Login"); } - public IActionResult OnPost(string provider, string returnUrl = null) + public async Task OnPost(string provider, string returnUrl = null) { - // Request a redirect to the external login provider. + if (IISDefaults.AuthenticationScheme == provider) + { + return await ProcessWindowsLoginAsync(returnUrl); + } + var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl }); var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); return new ChallengeResult(provider, properties); @@ -79,6 +87,10 @@ public async Task OnGetCallbackAsync(string returnUrl = null, str var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor : true); if (result.Succeeded) { + var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey); + + await UpdateClaims(info, user, "hasUsersGroup"); + _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider); return LocalRedirect(returnUrl); } @@ -124,6 +136,7 @@ public async Task OnPostConfirmationAsync(string returnUrl = null { await _signInManager.SignInAsync(user, isPersistent: false); _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider); + await UpdateClaims(info, user, "hasUsersGroup"); return LocalRedirect(returnUrl); } } @@ -137,5 +150,59 @@ public async Task OnPostConfirmationAsync(string returnUrl = null ReturnUrl = returnUrl; return Page(); } + + private async Task ProcessWindowsLoginAsync(string returnUrl) + { + var result = await HttpContext.AuthenticateAsync(IISDefaults.AuthenticationScheme); + if (result?.Principal is WindowsPrincipal wp) + { + var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl }); + + var props = _signInManager.ConfigureExternalAuthenticationProperties(IISDefaults.AuthenticationScheme, redirectUrl); + props.Items["scheme"] = IISDefaults.AuthenticationScheme; + + var id = new ClaimsIdentity(IISDefaults.AuthenticationScheme); + id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name)); + id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name)); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, wp.Identity.Name)); + + var wi = wp.Identity as WindowsIdentity; + var groups = wi.Groups.Translate(typeof(NTAccount)); + var hasUsersGroup = groups.Any(i => i.Value.Contains(@"BUILTIN\Users", StringComparison.OrdinalIgnoreCase)); + + id.AddClaim(new Claim("hasUsersGroup", hasUsersGroup.ToString())); + + await HttpContext.SignInAsync(IdentityConstants.ExternalScheme, new ClaimsPrincipal(id), props); + + return Redirect(props.RedirectUri); + } + + return Challenge(IISDefaults.AuthenticationScheme); + } + + private async Task UpdateClaims(ExternalLoginInfo info, ApplicationUser user, params string[] claimTypes) + { + if (claimTypes == null) + { + return; + } + + var claimTypesHash = new HashSet(claimTypes); + + var claims = (await _userManager.GetClaimsAsync(user)).Where(c => claimTypesHash.Contains(c.Type)).ToList(); + + await _userManager.RemoveClaimsAsync(user, claims); + + foreach (var claimType in claimTypes) + { + if (info.Principal.HasClaim(c => c.Type == claimType)) + { + claims = info.Principal.FindAll(claimType).ToList(); + await _userManager.AddClaimsAsync(user, claims); + } + } + } + + } } From 8734ce6d4af444234db7ddb8d798cf2915cef21f Mon Sep 17 00:00:00 2001 From: Alexander Kulik Date: Thu, 25 Jul 2019 16:04:43 +0300 Subject: [PATCH 4/6] Added windows claims support. --- src/ClientApp/src/app/app-routing.module.ts | 4 +++- src/ClientApp/src/app/app.component.html | 5 ++++- src/ClientApp/src/app/app.module.ts | 4 +++- .../app/internal-data/internal-data.component.ts | 15 +++++++++++++++ src/Controller/ExchangeRateController.cs | 8 ++++++++ src/Startup.cs | 6 ++++++ 6 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 src/ClientApp/src/app/internal-data/internal-data.component.ts diff --git a/src/ClientApp/src/app/app-routing.module.ts b/src/ClientApp/src/app/app-routing.module.ts index dc7bc08..e6bf42b 100644 --- a/src/ClientApp/src/app/app-routing.module.ts +++ b/src/ClientApp/src/app/app-routing.module.ts @@ -3,11 +3,13 @@ import { Routes, RouterModule } from '@angular/router'; import { HomeComponent } from "./home/home.component"; import { DataComponent } from "./data/data.component"; import { AuthorizeGuard } from "./api-authorization/authorize.guard"; +import { InternalDataComponent } from "./internal-data/internal-data.component"; const routes: Routes = [ { path: '', component: HomeComponent, pathMatch: 'full' }, - { path: 'data', component: DataComponent, canActivate: [AuthorizeGuard] } + { path: 'data', component: DataComponent, canActivate: [AuthorizeGuard] }, + { path: 'internaldata', component: InternalDataComponent, canActivate: [AuthorizeGuard] } ]; diff --git a/src/ClientApp/src/app/app.component.html b/src/ClientApp/src/app/app.component.html index c05b075..e854fd1 100644 --- a/src/ClientApp/src/app/app.component.html +++ b/src/ClientApp/src/app/app.component.html @@ -10,7 +10,10 @@ - + + diff --git a/src/ClientApp/src/app/app.module.ts b/src/ClientApp/src/app/app.module.ts index bccc12d..221643b 100644 --- a/src/ClientApp/src/app/app.module.ts +++ b/src/ClientApp/src/app/app.module.ts @@ -9,13 +9,15 @@ import { DataComponent } from './data/data.component'; import { ApiAuthorizationModule } from "./api-authorization/api-authorization.module"; import { ExchangeRateComponent } from './exchange-rate/exchange-rate.component'; import { AuthorizeInterceptor } from "./api-authorization/authorize.interceptor"; +import { InternalDataComponent } from './internal-data/internal-data.component'; @NgModule({ declarations: [ AppComponent, HomeComponent, DataComponent, - ExchangeRateComponent + ExchangeRateComponent, + InternalDataComponent ], imports: [ BrowserModule, diff --git a/src/ClientApp/src/app/internal-data/internal-data.component.ts b/src/ClientApp/src/app/internal-data/internal-data.component.ts new file mode 100644 index 0000000..1a03e11 --- /dev/null +++ b/src/ClientApp/src/app/internal-data/internal-data.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-internal-data', + template: ``, + styles: [] +}) +export class InternalDataComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/src/Controller/ExchangeRateController.cs b/src/Controller/ExchangeRateController.cs index d93bf99..930a853 100644 --- a/src/Controller/ExchangeRateController.cs +++ b/src/Controller/ExchangeRateController.cs @@ -28,5 +28,13 @@ public IEnumerable Get() }) .ToArray(); } + + [Authorize(Policy = "ShouldHasUsersGroup")] + [HttpGet("api/internalrates")] + public IEnumerable GetInternalRates() + { + return Get().Select(i => { i.Value = Math.Round(i.Value - 0.02, 2); return i; }); + } + } } diff --git a/src/Startup.cs b/src/Startup.cs index 98eced8..e9b9161 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -59,6 +59,12 @@ public void ConfigureServices(IServiceCollection services) services.AddControllers(); services.AddRazorPages(); + + services.AddAuthorization(options => + { + options.AddPolicy("ShouldHasUsersGroup", policy => { policy.RequireClaim("hasUsersGroup"); }); + }); + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. From 33245266aad51a2fa0a888635c1784a5739b68bb Mon Sep 17 00:00:00 2001 From: Alexander Kulik Date: Thu, 25 Jul 2019 16:07:14 +0300 Subject: [PATCH 5/6] Added configuration to identityserver. --- src/Startup.cs | 7 ++++++- src/data.db | Bin 126976 -> 126976 bytes 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Startup.cs b/src/Startup.cs index e9b9161..fde95d8 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -35,7 +35,12 @@ public void ConfigureServices(IServiceCollection services) services.AddDefaultIdentity() .AddEntityFrameworkStores(); services.AddIdentityServer() - .AddApiAuthorization(); + .AddApiAuthorization(options => + { + var apiResource = options.ApiResources.First(); + apiResource.UserClaims = new[] { "hasUsersGroup" }; + } + ); services.AddAuthentication() .AddOpenIdConnect("Google", "Google", diff --git a/src/data.db b/src/data.db index 1ada2a58da9da7169ff68a669213af74b08bb562..6f5ab7fdd26a6ba3a734915f4c9bd878b10fe6ce 100644 GIT binary patch delta 723 zcmZp8z~1nHeFIAX7c*}a13wF&DsR#$b`3Ig@Qu{- z4|2yPZW0pi9T?zm9^&H@5FQc{>J#N0W@PMVq7&&BWa{o}W}2F0Y-(thma1!HoMNbJ zVq%`Cn`Dt@q-&m*XlZ1Ulxl2lV7ys1?mi+?ec*cka=Gw{FOENJkMUtXAzS(nqXxWF&9Boyc&=bXgM++tn^1_nm{BMkgUfadSw z_poAQWe8OlCB}-3#A1*+#qLGSjR!NxsdharYTHnE2N-@UQ2;0Tf!yzj=LrXuxKc zhClL?4zRIoVc_2a6kf$YdCU6%Hb(vj4Ezr^3o2aW-~8aeO2c-R0>&Hi+waV0)DQrg da+!huGXF=Q^l|>_m-jOoq6%%le4bHP0RUVSFpK~I From 2820f590edfa36a124b54f5792cd891456fd27ba Mon Sep 17 00:00:00 2001 From: Alexander Kulik Date: Thu, 25 Jul 2019 16:09:56 +0300 Subject: [PATCH 6/6] Added aindows claims Guard into angular application. --- .../authorize-windows-group-guard.guard.ts | 26 +++++++++++++++++++ src/ClientApp/src/app/app-routing.module.ts | 3 ++- src/Startup.cs | 11 ++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/ClientApp/src/app/api-authorization/authorize-windows-group-guard.guard.ts diff --git a/src/ClientApp/src/app/api-authorization/authorize-windows-group-guard.guard.ts b/src/ClientApp/src/app/api-authorization/authorize-windows-group-guard.guard.ts new file mode 100644 index 0000000..57e54ee --- /dev/null +++ b/src/ClientApp/src/app/api-authorization/authorize-windows-group-guard.guard.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; +import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router'; +import { AuthorizeService } from "./authorize.service"; +import { ApplicationPaths, QueryParameterNames } from './api-authorization.constants'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthorizeWindowsGroupGuardGuard implements CanActivate { + constructor(private authorize: AuthorizeService, private router: Router) { } + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | + Promise | + boolean | + UrlTree { + return this.authorize.getUser().pipe(map((u: any) => !!u && !!u.hasUsersGroup)).pipe(tap((isAuthorized: boolean) => this.handleAuthorization(isAuthorized, state)));; + } + + private handleAuthorization(isAuthenticated: boolean, state: RouterStateSnapshot) { + if (!isAuthenticated) { + window.location.href = "/Identity/Account/Login?" + QueryParameterNames.ReturnUrl + "=/"; + } + } +} diff --git a/src/ClientApp/src/app/app-routing.module.ts b/src/ClientApp/src/app/app-routing.module.ts index e6bf42b..4760d6d 100644 --- a/src/ClientApp/src/app/app-routing.module.ts +++ b/src/ClientApp/src/app/app-routing.module.ts @@ -4,12 +4,13 @@ import { HomeComponent } from "./home/home.component"; import { DataComponent } from "./data/data.component"; import { AuthorizeGuard } from "./api-authorization/authorize.guard"; import { InternalDataComponent } from "./internal-data/internal-data.component"; +import { AuthorizeWindowsGroupGuardGuard } from "./api-authorization/authorize-windows-group-guard.guard"; const routes: Routes = [ { path: '', component: HomeComponent, pathMatch: 'full' }, { path: 'data', component: DataComponent, canActivate: [AuthorizeGuard] }, - { path: 'internaldata', component: InternalDataComponent, canActivate: [AuthorizeGuard] } + { path: 'internaldata', component: InternalDataComponent, canActivate: [AuthorizeWindowsGroupGuardGuard] } ]; diff --git a/src/Startup.cs b/src/Startup.cs index fde95d8..f4dfae9 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using IdentityServer4.Models; using IdentityServer4WebApp.Data; using IdentityServer4WebApp.Models; +using Microsoft.AspNetCore.ApiAuthorization.IdentityServer; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -39,6 +41,15 @@ public void ConfigureServices(IServiceCollection services) { var apiResource = options.ApiResources.First(); apiResource.UserClaims = new[] { "hasUsersGroup" }; + + var identityResource = new IdentityResource + { + Name = "customprofile", + DisplayName = "Custom profile", + UserClaims = new[] { "hasUsersGroup" }, + }; + identityResource.Properties.Add(ApplicationProfilesPropertyNames.Clients, "*"); + options.IdentityResources.Add(identityResource); } );